summaryrefslogtreecommitdiff
path: root/support/texlab/src/syntax/latex
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/syntax/latex')
-rw-r--r--support/texlab/src/syntax/latex/ast.rs345
-rw-r--r--support/texlab/src/syntax/latex/env.rs123
-rw-r--r--support/texlab/src/syntax/latex/finder.rs74
-rw-r--r--support/texlab/src/syntax/latex/glossary.rs58
-rw-r--r--support/texlab/src/syntax/latex/lexer.rs179
-rw-r--r--support/texlab/src/syntax/latex/math.rs199
-rw-r--r--support/texlab/src/syntax/latex/mod.rs358
-rw-r--r--support/texlab/src/syntax/latex/parser.rs135
-rw-r--r--support/texlab/src/syntax/latex/printer.rs77
-rw-r--r--support/texlab/src/syntax/latex/structure.rs250
10 files changed, 1798 insertions, 0 deletions
diff --git a/support/texlab/src/syntax/latex/ast.rs b/support/texlab/src/syntax/latex/ast.rs
new file mode 100644
index 0000000000..0af980b595
--- /dev/null
+++ b/support/texlab/src/syntax/latex/ast.rs
@@ -0,0 +1,345 @@
+use crate::range::RangeExt;
+use crate::syntax::text::{Span, SyntaxNode};
+use itertools::Itertools;
+use lsp_types::Range;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LatexTokenKind {
+ Word,
+ Command,
+ Math,
+ Comma,
+ BeginGroup,
+ EndGroup,
+ BeginOptions,
+ EndOptions,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexToken {
+ pub span: Span,
+ pub kind: LatexTokenKind,
+}
+
+impl LatexToken {
+ pub fn new(span: Span, kind: LatexTokenKind) -> Self {
+ Self { span, kind }
+ }
+
+ pub fn text(&self) -> &str {
+ &self.span.text
+ }
+}
+
+impl SyntaxNode for LatexToken {
+ fn range(&self) -> Range {
+ self.span.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Default)]
+pub struct LatexRoot {
+ pub children: Vec<LatexContent>,
+}
+
+impl LatexRoot {
+ pub fn new(children: Vec<LatexContent>) -> Self {
+ Self { children }
+ }
+}
+
+impl SyntaxNode for LatexRoot {
+ fn range(&self) -> Range {
+ if self.children.is_empty() {
+ Range::new_simple(0, 0, 0, 0)
+ } else {
+ Range::new(
+ self.children[0].start(),
+ self.children[self.children.len() - 1].end(),
+ )
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum LatexContent {
+ Group(Arc<LatexGroup>),
+ Command(Arc<LatexCommand>),
+ Text(Arc<LatexText>),
+ Comma(Arc<LatexComma>),
+ Math(Arc<LatexMath>),
+}
+
+impl LatexContent {
+ pub fn accept<T: LatexVisitor>(&self, visitor: &mut T) {
+ match self {
+ LatexContent::Group(group) => visitor.visit_group(Arc::clone(&group)),
+ LatexContent::Command(command) => visitor.visit_command(Arc::clone(&command)),
+ LatexContent::Text(text) => visitor.visit_text(Arc::clone(&text)),
+ LatexContent::Comma(comma) => visitor.visit_comma(Arc::clone(&comma)),
+ LatexContent::Math(math) => visitor.visit_math(Arc::clone(&math)),
+ }
+ }
+}
+
+impl SyntaxNode for LatexContent {
+ fn range(&self) -> Range {
+ match self {
+ LatexContent::Group(group) => group.range(),
+ LatexContent::Command(command) => command.range(),
+ LatexContent::Text(text) => text.range(),
+ LatexContent::Comma(comma) => comma.range(),
+ LatexContent::Math(math) => math.range(),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LatexGroupKind {
+ Group,
+ Options,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexGroup {
+ pub range: Range,
+ pub left: LatexToken,
+ pub children: Vec<LatexContent>,
+ pub right: Option<LatexToken>,
+ pub kind: LatexGroupKind,
+}
+
+impl LatexGroup {
+ pub fn new(
+ left: LatexToken,
+ children: Vec<LatexContent>,
+ right: Option<LatexToken>,
+ kind: LatexGroupKind,
+ ) -> Self {
+ let end = if let Some(ref right) = right {
+ right.end()
+ } else if !children.is_empty() {
+ children[children.len() - 1].end()
+ } else {
+ left.end()
+ };
+
+ Self {
+ range: Range::new(left.start(), end),
+ left,
+ children,
+ right,
+ kind,
+ }
+ }
+}
+
+impl SyntaxNode for LatexGroup {
+ fn range(&self) -> Range {
+ self.range
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexCommand {
+ pub range: Range,
+ pub name: LatexToken,
+ pub options: Vec<Arc<LatexGroup>>,
+ pub args: Vec<Arc<LatexGroup>>,
+ pub groups: Vec<Arc<LatexGroup>>,
+}
+
+impl LatexCommand {
+ pub fn new(
+ name: LatexToken,
+ options: Vec<Arc<LatexGroup>>,
+ args: Vec<Arc<LatexGroup>>,
+ ) -> Self {
+ let groups: Vec<Arc<LatexGroup>> = args
+ .iter()
+ .chain(options.iter())
+ .sorted_by_key(|group| group.range.start)
+ .map(Arc::clone)
+ .collect();
+
+ let end = if let Some(group) = groups.last() {
+ group.end()
+ } else {
+ name.end()
+ };
+
+ Self {
+ range: Range::new(name.start(), end),
+ name,
+ options,
+ args,
+ groups,
+ }
+ }
+
+ pub fn short_name_range(&self) -> Range {
+ Range::new_simple(
+ self.name.start().line,
+ self.name.start().character + 1,
+ self.name.end().line,
+ self.name.end().character,
+ )
+ }
+
+ pub fn extract_text(&self, index: usize) -> Option<&LatexText> {
+ if self.args.len() > index && self.args[index].children.len() == 1 {
+ if let LatexContent::Text(ref text) = self.args[index].children[0] {
+ Some(text)
+ } else {
+ None
+ }
+ } else {
+ None
+ }
+ }
+
+ pub fn extract_word(&self, index: usize) -> Option<&LatexToken> {
+ let text = self.extract_text(index)?;
+ if text.words.len() == 1 {
+ Some(&text.words[0])
+ } else {
+ None
+ }
+ }
+
+ pub fn has_word(&self, index: usize) -> bool {
+ self.extract_word(index).is_some()
+ }
+
+ pub fn extract_comma_separated_words(&self, index: usize) -> Vec<&LatexToken> {
+ let mut words = Vec::new();
+ for child in &self.args[index].children {
+ if let LatexContent::Text(text) = child {
+ for word in &text.words {
+ words.push(word);
+ }
+ }
+ }
+ words
+ }
+
+ pub fn has_comma_separated_words(&self, index: usize) -> bool {
+ if self.args.len() <= index {
+ return false;
+ }
+
+ for node in &self.args[index].children {
+ match node {
+ LatexContent::Text(_) | LatexContent::Comma(_) => (),
+ LatexContent::Command(_) | LatexContent::Group(_) | LatexContent::Math(_) => {
+ return false;
+ }
+ }
+ }
+ true
+ }
+}
+
+impl SyntaxNode for LatexCommand {
+ fn range(&self) -> Range {
+ self.range
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexText {
+ pub range: Range,
+ pub words: Vec<LatexToken>,
+}
+
+impl LatexText {
+ pub fn new(words: Vec<LatexToken>) -> Self {
+ Self {
+ range: Range::new(words[0].start(), words[words.len() - 1].end()),
+ words,
+ }
+ }
+}
+
+impl SyntaxNode for LatexText {
+ fn range(&self) -> Range {
+ self.range
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexComma {
+ pub token: LatexToken,
+}
+
+impl LatexComma {
+ pub fn new(token: LatexToken) -> Self {
+ Self { token }
+ }
+}
+
+impl SyntaxNode for LatexComma {
+ fn range(&self) -> Range {
+ self.token.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexMath {
+ pub token: LatexToken,
+}
+
+impl LatexMath {
+ pub fn new(token: LatexToken) -> Self {
+ Self { token }
+ }
+}
+
+impl SyntaxNode for LatexMath {
+ fn range(&self) -> Range {
+ self.token.range()
+ }
+}
+
+pub trait LatexVisitor {
+ fn visit_root(&mut self, root: Arc<LatexRoot>);
+
+ fn visit_group(&mut self, group: Arc<LatexGroup>);
+
+ fn visit_command(&mut self, command: Arc<LatexCommand>);
+
+ fn visit_text(&mut self, text: Arc<LatexText>);
+
+ fn visit_comma(&mut self, comma: Arc<LatexComma>);
+
+ fn visit_math(&mut self, math: Arc<LatexMath>);
+}
+
+pub struct LatexWalker;
+
+impl LatexWalker {
+ pub fn walk_root<T: LatexVisitor>(visitor: &mut T, root: Arc<LatexRoot>) {
+ for child in &root.children {
+ child.accept(visitor);
+ }
+ }
+
+ pub fn walk_group<T: LatexVisitor>(visitor: &mut T, group: Arc<LatexGroup>) {
+ for child in &group.children {
+ child.accept(visitor);
+ }
+ }
+
+ pub fn walk_command<T: LatexVisitor>(visitor: &mut T, command: Arc<LatexCommand>) {
+ for arg in &command.groups {
+ visitor.visit_group(Arc::clone(&arg));
+ }
+ }
+
+ pub fn walk_text<T: LatexVisitor>(_visitor: &mut T, _text: Arc<LatexText>) {}
+
+ pub fn walk_comma<T: LatexVisitor>(_visitor: &mut T, _comma: Arc<LatexComma>) {}
+
+ pub fn walk_math<T: LatexVisitor>(_visitor: &mut T, _math: Arc<LatexMath>) {}
+}
diff --git a/support/texlab/src/syntax/latex/env.rs b/support/texlab/src/syntax/latex/env.rs
new file mode 100644
index 0000000000..163db02ca8
--- /dev/null
+++ b/support/texlab/src/syntax/latex/env.rs
@@ -0,0 +1,123 @@
+use super::ast::*;
+use crate::syntax::language::*;
+use crate::syntax::text::SyntaxNode;
+use lsp_types::Range;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexEnvironmentDelimiter {
+ pub command: Arc<LatexCommand>,
+}
+
+impl LatexEnvironmentDelimiter {
+ pub fn name(&self) -> Option<&LatexToken> {
+ self.command.extract_word(0)
+ }
+
+ pub fn is_math(&self) -> bool {
+ if let Some(name) = self.name() {
+ LANGUAGE_DATA
+ .math_environments
+ .iter()
+ .any(|env| env == name.text())
+ } else {
+ false
+ }
+ }
+
+ pub fn is_enum(&self) -> bool {
+ if let Some(name) = self.name() {
+ LANGUAGE_DATA
+ .enum_environments
+ .iter()
+ .any(|env| env == name.text())
+ } else {
+ false
+ }
+ }
+}
+
+impl SyntaxNode for LatexEnvironmentDelimiter {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexEnvironment {
+ pub left: LatexEnvironmentDelimiter,
+ pub right: LatexEnvironmentDelimiter,
+}
+
+impl LatexEnvironment {
+ pub fn is_root(&self) -> bool {
+ self.left
+ .name()
+ .iter()
+ .chain(self.right.name().iter())
+ .any(|name| name.text() == "document")
+ }
+
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut stack = Vec::new();
+ let mut environments = Vec::new();
+ for command in commands {
+ if let Some(delimiter) = Self::parse_delimiter(command) {
+ if delimiter.command.name.text() == "\\begin" {
+ stack.push(delimiter);
+ } else if let Some(begin) = stack.pop() {
+ environments.push(Self {
+ left: begin,
+ right: delimiter,
+ });
+ }
+ }
+ }
+ environments
+ }
+
+ fn parse_delimiter(command: &Arc<LatexCommand>) -> Option<LatexEnvironmentDelimiter> {
+ if command.name.text() != "\\begin" && command.name.text() != "\\end" {
+ return None;
+ }
+
+ if command.args.is_empty() {
+ return None;
+ }
+
+ if command.has_word(0)
+ || command.args[0].children.is_empty()
+ || command.args[0].right.is_none()
+ {
+ let delimiter = LatexEnvironmentDelimiter {
+ command: Arc::clone(&command),
+ };
+ Some(delimiter)
+ } else {
+ None
+ }
+ }
+}
+
+impl SyntaxNode for LatexEnvironment {
+ fn range(&self) -> Range {
+ Range::new(self.left.start(), self.right.end())
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexEnvironmentInfo {
+ pub environments: Vec<LatexEnvironment>,
+ pub is_standalone: bool,
+}
+
+impl LatexEnvironmentInfo {
+ pub fn parse(commands: &[Arc<LatexCommand>]) -> Self {
+ let environments = LatexEnvironment::parse(commands);
+ let is_standalone = environments.iter().any(LatexEnvironment::is_root);
+ Self {
+ environments,
+ is_standalone,
+ }
+ }
+}
diff --git a/support/texlab/src/syntax/latex/finder.rs b/support/texlab/src/syntax/latex/finder.rs
new file mode 100644
index 0000000000..b59388dc07
--- /dev/null
+++ b/support/texlab/src/syntax/latex/finder.rs
@@ -0,0 +1,74 @@
+use super::ast::*;
+use crate::range::RangeExt;
+use crate::syntax::text::SyntaxNode;
+use lsp_types::Position;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub enum LatexNode {
+ Root(Arc<LatexRoot>),
+ Group(Arc<LatexGroup>),
+ Command(Arc<LatexCommand>),
+ Text(Arc<LatexText>),
+ Comma(Arc<LatexComma>),
+ Math(Arc<LatexMath>),
+}
+
+#[derive(Debug)]
+pub struct LatexFinder {
+ pub position: Position,
+ pub results: Vec<LatexNode>,
+}
+
+impl LatexFinder {
+ pub fn new(position: Position) -> Self {
+ Self {
+ position,
+ results: Vec::new(),
+ }
+ }
+}
+
+impl LatexVisitor for LatexFinder {
+ fn visit_root(&mut self, root: Arc<LatexRoot>) {
+ if root.range().contains(self.position) {
+ self.results.push(LatexNode::Root(Arc::clone(&root)));
+ LatexWalker::walk_root(self, root);
+ }
+ }
+
+ fn visit_group(&mut self, group: Arc<LatexGroup>) {
+ if group.range.contains(self.position) {
+ self.results.push(LatexNode::Group(Arc::clone(&group)));
+ LatexWalker::walk_group(self, group);
+ }
+ }
+
+ fn visit_command(&mut self, command: Arc<LatexCommand>) {
+ if command.range.contains(self.position) {
+ self.results.push(LatexNode::Command(Arc::clone(&command)));
+ LatexWalker::walk_command(self, command);
+ }
+ }
+
+ fn visit_text(&mut self, text: Arc<LatexText>) {
+ if text.range.contains(self.position) {
+ self.results.push(LatexNode::Text(Arc::clone(&text)));
+ LatexWalker::walk_text(self, text);
+ }
+ }
+
+ fn visit_comma(&mut self, comma: Arc<LatexComma>) {
+ if comma.range().contains(self.position) {
+ self.results.push(LatexNode::Comma(Arc::clone(&comma)));
+ LatexWalker::walk_comma(self, comma);
+ }
+ }
+
+ fn visit_math(&mut self, math: Arc<LatexMath>) {
+ if math.range().contains(self.position) {
+ self.results.push(LatexNode::Math(Arc::clone(&math)));
+ LatexWalker::walk_math(self, math);
+ }
+ }
+}
diff --git a/support/texlab/src/syntax/latex/glossary.rs b/support/texlab/src/syntax/latex/glossary.rs
new file mode 100644
index 0000000000..14f87ef130
--- /dev/null
+++ b/support/texlab/src/syntax/latex/glossary.rs
@@ -0,0 +1,58 @@
+use super::ast::*;
+use crate::syntax::language::*;
+use crate::syntax::text::SyntaxNode;
+use lsp_types::Range;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexGlossaryEntry {
+ pub command: Arc<LatexCommand>,
+ pub label_index: usize,
+ pub kind: LatexGlossaryEntryKind,
+}
+
+impl SyntaxNode for LatexGlossaryEntry {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+impl LatexGlossaryEntry {
+ pub fn label(&self) -> &LatexToken {
+ self.command.extract_word(self.label_index).unwrap()
+ }
+
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut entries = Vec::new();
+ for command in commands {
+ for LatexGlossaryEntryDefinitionCommand {
+ name,
+ label_index,
+ kind,
+ } in &LANGUAGE_DATA.glossary_entry_definition_commands
+ {
+ if command.name.text() == name && command.has_word(*label_index) {
+ entries.push(Self {
+ command: Arc::clone(&command),
+ label_index: *label_index,
+ kind: *kind,
+ });
+ }
+ }
+ }
+ entries
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexGlossaryInfo {
+ pub entries: Vec<LatexGlossaryEntry>,
+}
+
+impl LatexGlossaryInfo {
+ pub fn parse(commands: &[Arc<LatexCommand>]) -> Self {
+ Self {
+ entries: LatexGlossaryEntry::parse(commands),
+ }
+ }
+}
diff --git a/support/texlab/src/syntax/latex/lexer.rs b/support/texlab/src/syntax/latex/lexer.rs
new file mode 100644
index 0000000000..f753d7afce
--- /dev/null
+++ b/support/texlab/src/syntax/latex/lexer.rs
@@ -0,0 +1,179 @@
+use super::ast::{LatexToken, LatexTokenKind};
+use crate::syntax::text::CharStream;
+
+pub struct LatexLexer<'a> {
+ stream: CharStream<'a>,
+}
+
+impl<'a> LatexLexer<'a> {
+ pub fn new(text: &'a str) -> Self {
+ LatexLexer {
+ stream: CharStream::new(text),
+ }
+ }
+
+ fn single_char(&mut self, kind: LatexTokenKind) -> LatexToken {
+ self.stream.start_span();
+ self.stream.next();
+ let span = self.stream.end_span();
+ LatexToken::new(span, kind)
+ }
+
+ fn math(&mut self) -> LatexToken {
+ self.stream.start_span();
+ self.stream.next();
+ if self.stream.satifies(|c| *c == '$') {
+ self.stream.next();
+ }
+ let span = self.stream.end_span();
+ LatexToken::new(span, LatexTokenKind::Math)
+ }
+
+ fn command(&mut self) -> LatexToken {
+ let span = self.stream.command();
+ LatexToken::new(span, LatexTokenKind::Command)
+ }
+
+ fn word(&mut self) -> LatexToken {
+ self.stream.start_span();
+ self.stream.next();
+ while self.stream.satifies(|c| is_word_char(*c)) {
+ self.stream.next();
+ }
+
+ let span = self.stream.end_span();
+ LatexToken::new(span, LatexTokenKind::Word)
+ }
+}
+
+impl<'a> Iterator for LatexLexer<'a> {
+ type Item = LatexToken;
+
+ fn next(&mut self) -> Option<LatexToken> {
+ loop {
+ match self.stream.peek() {
+ Some('%') => {
+ self.stream.skip_rest_of_line();
+ }
+ Some('{') => {
+ return Some(self.single_char(LatexTokenKind::BeginGroup));
+ }
+ Some('}') => {
+ return Some(self.single_char(LatexTokenKind::EndGroup));
+ }
+ Some('[') => {
+ return Some(self.single_char(LatexTokenKind::BeginOptions));
+ }
+ Some(']') => {
+ return Some(self.single_char(LatexTokenKind::EndOptions));
+ }
+ Some('$') => {
+ return Some(self.math());
+ }
+ Some(',') => {
+ return Some(self.single_char(LatexTokenKind::Comma));
+ }
+ Some('\\') => {
+ return Some(self.command());
+ }
+ Some(c) => {
+ if c.is_whitespace() {
+ self.stream.next();
+ } else {
+ return Some(self.word());
+ }
+ }
+ None => {
+ return None;
+ }
+ }
+ }
+ }
+}
+
+fn is_word_char(c: char) -> bool {
+ !c.is_whitespace()
+ && c != '%'
+ && c != '{'
+ && c != '}'
+ && c != '['
+ && c != ']'
+ && c != '\\'
+ && c != '$'
+ && c != ','
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::syntax::text::Span;
+ use lsp_types::{Position, Range};
+
+ fn verify<'a>(
+ lexer: &mut LatexLexer<'a>,
+ line: u64,
+ character: u64,
+ text: &str,
+ kind: LatexTokenKind,
+ ) {
+ 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 = LatexToken::new(span, kind);
+ assert_eq!(Some(token), lexer.next());
+ }
+
+ #[test]
+ fn test_word() {
+ let mut lexer = LatexLexer::new("foo bar baz");
+ verify(&mut lexer, 0, 0, "foo", LatexTokenKind::Word);
+ verify(&mut lexer, 0, 4, "bar", LatexTokenKind::Word);
+ verify(&mut lexer, 0, 8, "baz", LatexTokenKind::Word);
+ assert_eq!(None, lexer.next());
+ }
+
+ #[test]
+ fn test_command() {
+ let mut lexer = LatexLexer::new("\\foo\\bar@baz\n\\foo*");
+ verify(&mut lexer, 0, 0, "\\foo", LatexTokenKind::Command);
+ verify(&mut lexer, 0, 4, "\\bar@baz", LatexTokenKind::Command);
+ verify(&mut lexer, 1, 0, "\\foo*", LatexTokenKind::Command);
+ assert_eq!(None, lexer.next());
+ }
+
+ #[test]
+ fn test_escape_sequence() {
+ let mut lexer = LatexLexer::new("\\%\\**");
+ verify(&mut lexer, 0, 0, "\\%", LatexTokenKind::Command);
+ verify(&mut lexer, 0, 2, "\\*", LatexTokenKind::Command);
+ verify(&mut lexer, 0, 4, "*", LatexTokenKind::Word);
+ assert_eq!(None, lexer.next());
+ }
+
+ #[test]
+ fn test_group_delimiter() {
+ let mut lexer = LatexLexer::new("{}[]");
+ verify(&mut lexer, 0, 0, "{", LatexTokenKind::BeginGroup);
+ verify(&mut lexer, 0, 1, "}", LatexTokenKind::EndGroup);
+ verify(&mut lexer, 0, 2, "[", LatexTokenKind::BeginOptions);
+ verify(&mut lexer, 0, 3, "]", LatexTokenKind::EndOptions);
+ assert_eq!(None, lexer.next());
+ }
+
+ #[test]
+ fn test_math() {
+ let mut lexer = LatexLexer::new("$$ $ $");
+ verify(&mut lexer, 0, 0, "$$", LatexTokenKind::Math);
+ verify(&mut lexer, 0, 3, "$", LatexTokenKind::Math);
+ verify(&mut lexer, 0, 5, "$", LatexTokenKind::Math);
+ assert_eq!(None, lexer.next());
+ }
+
+ #[test]
+ fn test_line_comment() {
+ let mut lexer = LatexLexer::new(" %foo \nfoo");
+ verify(&mut lexer, 1, 0, "foo", LatexTokenKind::Word);
+ assert_eq!(None, lexer.next());
+ }
+}
diff --git a/support/texlab/src/syntax/latex/math.rs b/support/texlab/src/syntax/latex/math.rs
new file mode 100644
index 0000000000..c234232c14
--- /dev/null
+++ b/support/texlab/src/syntax/latex/math.rs
@@ -0,0 +1,199 @@
+use super::ast::*;
+use crate::syntax::language::*;
+use crate::syntax::text::SyntaxNode;
+use lsp_types::Range;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexEquation {
+ pub left: Arc<LatexCommand>,
+ pub right: Arc<LatexCommand>,
+}
+
+impl LatexEquation {
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut equations = Vec::new();
+ let mut left = None;
+ for command in commands {
+ let name = command.name.text();
+ if name == "\\[" || name == "\\(" {
+ left = Some(command);
+ } else if name == "\\]" || name == "\\)" {
+ if let Some(begin) = left {
+ equations.push(Self {
+ left: Arc::clone(&begin),
+ right: Arc::clone(&command),
+ });
+ left = None;
+ }
+ }
+ }
+ equations
+ }
+}
+
+impl SyntaxNode for LatexEquation {
+ fn range(&self) -> Range {
+ Range::new(self.left.start(), self.right.end())
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexInline {
+ pub left: Arc<LatexMath>,
+ pub right: Arc<LatexMath>,
+}
+
+impl LatexInline {
+ fn parse(root: Arc<LatexRoot>) -> Vec<Self> {
+ let mut analyzer = LatexInlineAnalyzer::default();
+ analyzer.visit_root(root);
+ analyzer.inlines
+ }
+}
+
+impl SyntaxNode for LatexInline {
+ fn range(&self) -> Range {
+ Range::new(self.left.start(), self.right.end())
+ }
+}
+
+#[derive(Debug, Default)]
+struct LatexInlineAnalyzer {
+ inlines: Vec<LatexInline>,
+ left: Option<Arc<LatexMath>>,
+}
+
+impl LatexVisitor for LatexInlineAnalyzer {
+ fn visit_root(&mut self, root: Arc<LatexRoot>) {
+ LatexWalker::walk_root(self, root);
+ }
+
+ fn visit_group(&mut self, group: Arc<LatexGroup>) {
+ LatexWalker::walk_group(self, group);
+ }
+
+ fn visit_command(&mut self, command: Arc<LatexCommand>) {
+ LatexWalker::walk_command(self, command);
+ }
+
+ fn visit_text(&mut self, text: Arc<LatexText>) {
+ LatexWalker::walk_text(self, text);
+ }
+
+ fn visit_comma(&mut self, comma: Arc<LatexComma>) {
+ LatexWalker::walk_comma(self, comma);
+ }
+
+ fn visit_math(&mut self, math: Arc<LatexMath>) {
+ if let Some(left) = &self.left {
+ let inline = LatexInline {
+ left: Arc::clone(&left),
+ right: Arc::clone(&math),
+ };
+ self.inlines.push(inline);
+ self.left = None;
+ } else {
+ self.left = Some(Arc::clone(&math));
+ }
+ LatexWalker::walk_math(self, math);
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexMathOperator {
+ pub command: Arc<LatexCommand>,
+ pub definition: Arc<LatexCommand>,
+ pub definition_index: usize,
+ pub implementation_index: usize,
+}
+
+impl LatexMathOperator {
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut operators = Vec::new();
+ for command in commands {
+ for LatexMathOperatorCommand {
+ name,
+ definition_index,
+ implementation_index,
+ } in &LANGUAGE_DATA.math_operator_commands
+ {
+ if command.name.text() == name
+ && command.args.len() > *definition_index
+ && command.args.len() > *implementation_index
+ {
+ let definition = command.args[0].children.iter().next();
+ if let Some(LatexContent::Command(definition)) = definition {
+ operators.push(Self {
+ command: Arc::clone(command),
+ definition: Arc::clone(definition),
+ definition_index: *definition_index,
+ implementation_index: *implementation_index,
+ })
+ }
+ }
+ }
+ }
+ operators
+ }
+}
+
+impl SyntaxNode for LatexMathOperator {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexTheoremDefinition {
+ pub command: Arc<LatexCommand>,
+ pub index: usize,
+}
+
+impl LatexTheoremDefinition {
+ pub fn name(&self) -> &LatexToken {
+ self.command.extract_word(self.index).unwrap()
+ }
+
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut definitions = Vec::new();
+ for command in commands {
+ for LatexTheoremDefinitionCommand { name, index } in
+ &LANGUAGE_DATA.theorem_definition_commands
+ {
+ if command.name.text() == name && command.has_word(*index) {
+ definitions.push(Self {
+ command: Arc::clone(&command),
+ index: *index,
+ });
+ }
+ }
+ }
+ definitions
+ }
+}
+
+impl SyntaxNode for LatexTheoremDefinition {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexMathInfo {
+ pub equations: Vec<LatexEquation>,
+ pub inlines: Vec<LatexInline>,
+ pub operators: Vec<LatexMathOperator>,
+ pub theorem_definitions: Vec<LatexTheoremDefinition>,
+}
+
+impl LatexMathInfo {
+ pub fn parse(root: Arc<LatexRoot>, commands: &[Arc<LatexCommand>]) -> Self {
+ Self {
+ equations: LatexEquation::parse(commands),
+ inlines: LatexInline::parse(root),
+ operators: LatexMathOperator::parse(commands),
+ theorem_definitions: LatexTheoremDefinition::parse(commands),
+ }
+ }
+}
diff --git a/support/texlab/src/syntax/latex/mod.rs b/support/texlab/src/syntax/latex/mod.rs
new file mode 100644
index 0000000000..dc24682752
--- /dev/null
+++ b/support/texlab/src/syntax/latex/mod.rs
@@ -0,0 +1,358 @@
+mod ast;
+mod env;
+mod finder;
+mod glossary;
+mod lexer;
+mod math;
+mod parser;
+mod printer;
+mod structure;
+
+pub use self::ast::*;
+pub use self::env::*;
+pub use self::finder::LatexNode;
+pub use self::glossary::*;
+pub use self::math::*;
+pub use self::printer::LatexPrinter;
+pub use self::structure::*;
+
+use self::finder::LatexFinder;
+use self::lexer::LatexLexer;
+use self::parser::LatexParser;
+use super::language::*;
+use super::text::SyntaxNode;
+use crate::range::RangeExt;
+use crate::workspace::Uri;
+use lsp_types::{Position, Range};
+use path_clean::PathClean;
+use std::path::PathBuf;
+use std::sync::Arc;
+
+#[derive(Debug, Default)]
+struct LatexCommandAnalyzer {
+ commands: Vec<Arc<LatexCommand>>,
+}
+
+impl LatexCommandAnalyzer {
+ fn parse(root: Arc<LatexRoot>) -> Vec<Arc<LatexCommand>> {
+ let mut analyzer = Self::default();
+ analyzer.visit_root(root);
+ analyzer.commands
+ }
+}
+
+impl LatexVisitor for LatexCommandAnalyzer {
+ fn visit_root(&mut self, root: Arc<LatexRoot>) {
+ LatexWalker::walk_root(self, root);
+ }
+
+ fn visit_group(&mut self, group: Arc<LatexGroup>) {
+ LatexWalker::walk_group(self, group);
+ }
+
+ fn visit_command(&mut self, command: Arc<LatexCommand>) {
+ self.commands.push(Arc::clone(&command));
+ LatexWalker::walk_command(self, command);
+ }
+
+ fn visit_text(&mut self, text: Arc<LatexText>) {
+ LatexWalker::walk_text(self, text);
+ }
+
+ fn visit_comma(&mut self, comma: Arc<LatexComma>) {
+ LatexWalker::walk_comma(self, comma);
+ }
+
+ fn visit_math(&mut self, math: Arc<LatexMath>) {
+ LatexWalker::walk_math(self, math);
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexCitation {
+ pub command: Arc<LatexCommand>,
+ pub index: usize,
+}
+
+impl LatexCitation {
+ pub fn keys(&self) -> Vec<&LatexToken> {
+ self.command.extract_comma_separated_words(0)
+ }
+
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut citations = Vec::new();
+ for command in commands {
+ for LatexCitationCommand { name, index } in &LANGUAGE_DATA.citation_commands {
+ if command.name.text() == name && command.has_comma_separated_words(*index) {
+ citations.push(Self {
+ command: Arc::clone(command),
+ index: *index,
+ });
+ }
+ }
+ }
+ citations
+ }
+}
+
+impl SyntaxNode for LatexCitation {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexInclude {
+ pub command: Arc<LatexCommand>,
+ pub index: usize,
+ pub kind: LatexIncludeKind,
+ pub all_targets: Vec<Vec<Uri>>,
+ pub include_extension: bool,
+}
+
+impl LatexInclude {
+ pub fn paths(&self) -> Vec<&LatexToken> {
+ self.command.extract_comma_separated_words(self.index)
+ }
+
+ pub fn components(&self) -> Vec<String> {
+ let mut components = Vec::new();
+ for path in self.paths() {
+ match self.kind {
+ LatexIncludeKind::Package => components.push(format!("{}.sty", path.text())),
+ LatexIncludeKind::Class => components.push(format!("{}.cls", path.text())),
+ LatexIncludeKind::Latex
+ | LatexIncludeKind::Bibliography
+ | LatexIncludeKind::Image
+ | LatexIncludeKind::Svg
+ | LatexIncludeKind::Pdf
+ | LatexIncludeKind::Everything => (),
+ }
+ }
+ components
+ }
+
+ fn parse(uri: &Uri, commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut includes = Vec::new();
+ for command in commands {
+ for description in &LANGUAGE_DATA.include_commands {
+ if let Some(include) = Self::parse_single(uri, &command, &description) {
+ includes.push(include);
+ }
+ }
+ }
+ includes
+ }
+
+ fn parse_single(
+ uri: &Uri,
+ command: &Arc<LatexCommand>,
+ description: &LatexIncludeCommand,
+ ) -> Option<Self> {
+ if command.name.text() != description.name {
+ return None;
+ }
+
+ if command.args.len() <= description.index {
+ return None;
+ }
+
+ let mut all_targets = Vec::new();
+ for relative_path in command.extract_comma_separated_words(description.index) {
+ let mut path = uri.to_file_path().ok()?;
+ path.pop();
+ path.push(relative_path.text());
+ path = PathBuf::from(path.to_string_lossy().into_owned().replace('\\', "/"));
+ path = path.clean();
+ let path = path.to_str()?.to_owned();
+
+ let mut targets = Vec::new();
+ targets.push(Uri::from_file_path(&path).ok()?);
+ if let Some(extensions) = description.kind.extensions() {
+ for extension in extensions {
+ let path = format!("{}.{}", &path, extension);
+ targets.push(Uri::from_file_path(&path).ok()?);
+ }
+ }
+ all_targets.push(targets);
+ }
+
+ let include = Self {
+ command: Arc::clone(command),
+ index: description.index,
+ kind: description.kind,
+ all_targets,
+ include_extension: description.include_extension,
+ };
+ Some(include)
+ }
+}
+
+impl SyntaxNode for LatexInclude {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexCommandDefinition {
+ pub command: Arc<LatexCommand>,
+ pub definition: Arc<LatexCommand>,
+ pub definition_index: usize,
+ pub implementation: Arc<LatexGroup>,
+ pub implementation_index: usize,
+ pub argument_count_index: usize,
+}
+
+impl LatexCommandDefinition {
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut definitions = Vec::new();
+ for command in commands {
+ for LatexCommandDefinitionCommand {
+ name,
+ definition_index,
+ argument_count_index,
+ implementation_index,
+ } in &LANGUAGE_DATA.command_definition_commands
+ {
+ if command.name.text() == name
+ && command.args.len() > *definition_index
+ && command.args.len() > *implementation_index
+ {
+ let definition = command.args[0].children.iter().next();
+ if let Some(LatexContent::Command(definition)) = definition {
+ definitions.push(Self {
+ command: Arc::clone(command),
+ definition: Arc::clone(definition),
+ definition_index: *definition_index,
+ implementation: Arc::clone(&command.args[*implementation_index]),
+ implementation_index: *implementation_index,
+ argument_count_index: *argument_count_index,
+ })
+ }
+ }
+ }
+ }
+ definitions
+ }
+}
+
+impl SyntaxNode for LatexCommandDefinition {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexSyntaxTree {
+ pub root: Arc<LatexRoot>,
+ pub commands: Vec<Arc<LatexCommand>>,
+ pub includes: Vec<LatexInclude>,
+ pub components: Vec<String>,
+ pub env: LatexEnvironmentInfo,
+ pub structure: LatexStructureInfo,
+ pub citations: Vec<LatexCitation>,
+ pub math: LatexMathInfo,
+ pub command_definitions: Vec<LatexCommandDefinition>,
+ pub glossary: LatexGlossaryInfo,
+}
+
+impl LatexSyntaxTree {
+ pub fn parse(uri: &Uri, text: &str) -> Self {
+ let lexer = LatexLexer::new(text);
+ let mut parser = LatexParser::new(lexer);
+ let root = Arc::new(parser.root());
+ let commands = LatexCommandAnalyzer::parse(Arc::clone(&root));
+ let includes = LatexInclude::parse(uri, &commands);
+ let components = includes.iter().flat_map(LatexInclude::components).collect();
+ let env = LatexEnvironmentInfo::parse(&commands);
+ let structure = LatexStructureInfo::parse(&commands);
+ let citations = LatexCitation::parse(&commands);
+ let math = LatexMathInfo::parse(Arc::clone(&root), &commands);
+ let command_definitions = LatexCommandDefinition::parse(&commands);
+ let glossary = LatexGlossaryInfo::parse(&commands);
+ Self {
+ root,
+ commands,
+ includes,
+ components,
+ env,
+ structure,
+ citations,
+ math,
+ command_definitions,
+ glossary,
+ }
+ }
+
+ pub fn find(&self, position: Position) -> Vec<LatexNode> {
+ let mut finder = LatexFinder::new(position);
+ finder.visit_root(Arc::clone(&self.root));
+ finder.results
+ }
+
+ pub fn find_command_by_name(&self, position: Position) -> Option<Arc<LatexCommand>> {
+ for result in self.find(position) {
+ if let LatexNode::Command(command) = result {
+ if command.name.range().contains(position)
+ && command.name.start().character != position.character
+ {
+ return Some(command);
+ }
+ }
+ }
+ None
+ }
+
+ pub fn find_label_by_range(&self, range: Range) -> Option<&LatexLabel> {
+ self.structure
+ .labels
+ .iter()
+ .filter(|label| label.kind == LatexLabelKind::Definition)
+ .filter(|label| label.names().len() == 1)
+ .find(|label| range.contains(label.start()))
+ }
+
+ pub fn find_label_by_environment(&self, environment: &LatexEnvironment) -> Option<&LatexLabel> {
+ self.structure
+ .labels
+ .iter()
+ .filter(|label| label.kind == LatexLabelKind::Definition)
+ .filter(|label| label.names().len() == 1)
+ .find(|label| self.is_direct_child(environment, label.start()))
+ }
+
+ pub fn is_enumeration_item(&self, enumeration: &LatexEnvironment, item: &LatexItem) -> bool {
+ enumeration.range().contains(item.start())
+ && !self
+ .env
+ .environments
+ .iter()
+ .filter(|env| *env != enumeration)
+ .filter(|env| env.left.is_enum() && enumeration.range().contains(env.start()))
+ .any(|env| env.range().contains(item.start()))
+ }
+
+ pub fn is_direct_child(&self, environment: &LatexEnvironment, position: Position) -> bool {
+ environment.range().contains(position)
+ && !self
+ .env
+ .environments
+ .iter()
+ .filter(|env| *env != environment)
+ .filter(|env| environment.range().contains(env.start()))
+ .any(|env| env.range().contains(position))
+ }
+}
+
+pub fn extract_group(content: &LatexGroup) -> String {
+ if content.children.is_empty() || content.right.is_none() {
+ return String::new();
+ }
+
+ let mut printer = LatexPrinter::new(content.children[0].start());
+ for child in &content.children {
+ child.accept(&mut printer);
+ }
+ printer.output
+}
diff --git a/support/texlab/src/syntax/latex/parser.rs b/support/texlab/src/syntax/latex/parser.rs
new file mode 100644
index 0000000000..40693071c2
--- /dev/null
+++ b/support/texlab/src/syntax/latex/parser.rs
@@ -0,0 +1,135 @@
+use super::ast::*;
+use std::iter::Peekable;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+enum LatexScope {
+ Root,
+ Group,
+ Options,
+}
+
+pub struct LatexParser<I: Iterator<Item = LatexToken>> {
+ tokens: Peekable<I>,
+}
+
+impl<I: Iterator<Item = LatexToken>> LatexParser<I> {
+ pub fn new(tokens: I) -> Self {
+ LatexParser {
+ tokens: tokens.peekable(),
+ }
+ }
+
+ pub fn root(&mut self) -> LatexRoot {
+ let children = self.content(LatexScope::Root);
+ LatexRoot::new(children)
+ }
+
+ fn content(&mut self, scope: LatexScope) -> Vec<LatexContent> {
+ let mut children = Vec::new();
+ while let Some(ref token) = self.tokens.peek() {
+ match token.kind {
+ LatexTokenKind::Word | LatexTokenKind::BeginOptions => {
+ children.push(LatexContent::Text(self.text(scope)));
+ }
+ LatexTokenKind::Command => {
+ children.push(LatexContent::Command(self.command()));
+ }
+ LatexTokenKind::Comma => {
+ let node = LatexComma::new(self.tokens.next().unwrap());
+ children.push(LatexContent::Comma(Arc::new(node)));
+ }
+ LatexTokenKind::Math => {
+ children.push(LatexContent::Math(self.math()));
+ }
+ LatexTokenKind::BeginGroup => {
+ children.push(LatexContent::Group(self.group(LatexGroupKind::Group)));
+ }
+ LatexTokenKind::EndGroup => {
+ if scope == LatexScope::Root {
+ self.tokens.next();
+ } else {
+ return children;
+ }
+ }
+ LatexTokenKind::EndOptions => {
+ if scope == LatexScope::Options {
+ return children;
+ } else {
+ children.push(LatexContent::Text(self.text(scope)));
+ }
+ }
+ }
+ }
+ children
+ }
+
+ fn command(&mut self) -> Arc<LatexCommand> {
+ let name = self.tokens.next().unwrap();
+
+ let mut options = Vec::new();
+ let mut args = Vec::new();
+ while let Some(token) = self.tokens.peek() {
+ match token.kind {
+ LatexTokenKind::BeginGroup => {
+ args.push(self.group(LatexGroupKind::Group));
+ }
+ LatexTokenKind::BeginOptions => {
+ options.push(self.group(LatexGroupKind::Options));
+ }
+ _ => {
+ break;
+ }
+ }
+ }
+ Arc::new(LatexCommand::new(name, options, args))
+ }
+
+ fn group(&mut self, kind: LatexGroupKind) -> Arc<LatexGroup> {
+ let left = self.tokens.next().unwrap();
+ let scope = match kind {
+ LatexGroupKind::Group => LatexScope::Group,
+ LatexGroupKind::Options => LatexScope::Options,
+ };
+ let children = self.content(scope);
+ let right_kind = match kind {
+ LatexGroupKind::Group => LatexTokenKind::EndGroup,
+ LatexGroupKind::Options => LatexTokenKind::EndOptions,
+ };
+
+ let right = if self.next_of_kind(right_kind) {
+ self.tokens.next()
+ } else {
+ None
+ };
+
+ Arc::new(LatexGroup::new(left, children, right, kind))
+ }
+
+ fn text(&mut self, scope: LatexScope) -> Arc<LatexText> {
+ let mut words = Vec::new();
+ while let Some(ref token) = self.tokens.peek() {
+ let kind = token.kind;
+ let opts = kind == LatexTokenKind::EndOptions && scope != LatexScope::Options;
+ if kind == LatexTokenKind::Word || kind == LatexTokenKind::BeginOptions || opts {
+ words.push(self.tokens.next().unwrap());
+ } else {
+ break;
+ }
+ }
+ Arc::new(LatexText::new(words))
+ }
+
+ fn math(&mut self) -> Arc<LatexMath> {
+ let token = self.tokens.next().unwrap();
+ Arc::new(LatexMath::new(token))
+ }
+
+ fn next_of_kind(&mut self, kind: LatexTokenKind) -> bool {
+ if let Some(ref token) = self.tokens.peek() {
+ token.kind == kind
+ } else {
+ false
+ }
+ }
+}
diff --git a/support/texlab/src/syntax/latex/printer.rs b/support/texlab/src/syntax/latex/printer.rs
new file mode 100644
index 0000000000..ce03778033
--- /dev/null
+++ b/support/texlab/src/syntax/latex/printer.rs
@@ -0,0 +1,77 @@
+use super::ast::*;
+use crate::syntax::text::*;
+use lsp_types::Position;
+use std::sync::Arc;
+
+#[derive(Debug)]
+pub struct LatexPrinter {
+ pub output: String,
+ position: Position,
+}
+
+impl LatexPrinter {
+ pub fn new(start_position: Position) -> Self {
+ Self {
+ output: String::new(),
+ position: start_position,
+ }
+ }
+
+ fn synchronize(&mut self, position: Position) {
+ while self.position.line < position.line {
+ self.output.push('\n');
+ self.position.line += 1;
+ }
+
+ while self.position.character < position.character {
+ self.output.push(' ');
+ self.position.character += 1;
+ }
+ }
+
+ fn print_token(&mut self, token: &LatexToken) {
+ self.synchronize(token.start());
+ self.output.push_str(token.text());
+ self.position.character += token.end().character - token.start().character;
+ self.synchronize(token.end());
+ }
+}
+
+impl LatexVisitor for LatexPrinter {
+ fn visit_root(&mut self, root: Arc<LatexRoot>) {
+ for child in &root.children {
+ child.accept(self);
+ }
+ }
+
+ fn visit_group(&mut self, group: Arc<LatexGroup>) {
+ self.print_token(&group.left);
+ for child in &group.children {
+ child.accept(self);
+ }
+ if let Some(right) = &group.right {
+ self.print_token(right);
+ }
+ }
+
+ fn visit_command(&mut self, command: Arc<LatexCommand>) {
+ self.print_token(&command.name);
+ for group in &command.groups {
+ self.visit_group(Arc::clone(&group));
+ }
+ }
+
+ fn visit_text(&mut self, text: Arc<LatexText>) {
+ for word in &text.words {
+ self.print_token(word);
+ }
+ }
+
+ fn visit_comma(&mut self, comma: Arc<LatexComma>) {
+ self.print_token(&comma.token);
+ }
+
+ fn visit_math(&mut self, math: Arc<LatexMath>) {
+ self.print_token(&math.token)
+ }
+}
diff --git a/support/texlab/src/syntax/latex/structure.rs b/support/texlab/src/syntax/latex/structure.rs
new file mode 100644
index 0000000000..79c9aac216
--- /dev/null
+++ b/support/texlab/src/syntax/latex/structure.rs
@@ -0,0 +1,250 @@
+use super::ast::*;
+use crate::range::RangeExt;
+use crate::syntax::language::*;
+use crate::syntax::text::{CharStream, SyntaxNode};
+use itertools::Itertools;
+use lsp_types::Range;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexSection {
+ pub command: Arc<LatexCommand>,
+ pub index: usize,
+ pub level: i32,
+ pub prefix: &'static str,
+}
+
+impl LatexSection {
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut sections = Vec::new();
+ for command in commands {
+ for LatexSectionCommand {
+ name,
+ index,
+ level,
+ prefix,
+ } in &LANGUAGE_DATA.section_commands
+ {
+ if command.name.text() == name && command.args.len() > *index {
+ sections.push(Self {
+ command: Arc::clone(command),
+ index: *index,
+ level: *level,
+ prefix: prefix.as_ref(),
+ })
+ }
+ }
+ }
+ sections
+ }
+
+ pub fn extract_text(&self, text: &str) -> Option<String> {
+ let content = &self.command.args[self.index];
+ let right = content.right.as_ref()?;
+ let range = Range::new_simple(
+ content.left.start().line,
+ content.left.start().character + 1,
+ right.end().line,
+ right.end().character - 1,
+ );
+ Some(CharStream::extract(&text, range))
+ }
+}
+
+impl SyntaxNode for LatexSection {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexLabel {
+ pub command: Arc<LatexCommand>,
+ index: usize,
+ pub kind: LatexLabelKind,
+}
+
+impl LatexLabel {
+ pub fn names(&self) -> Vec<&LatexToken> {
+ self.command.extract_comma_separated_words(self.index)
+ }
+
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut labels = Vec::new();
+ for command in commands {
+ for LatexLabelCommand { name, index, kind } in &LANGUAGE_DATA.label_commands {
+ if command.name.text() == name && command.has_comma_separated_words(*index) {
+ labels.push(Self {
+ command: Arc::clone(command),
+ index: *index,
+ kind: *kind,
+ });
+ }
+ }
+ }
+ labels
+ }
+}
+
+impl SyntaxNode for LatexLabel {
+ fn range(&self) -> Range {
+ self.command.range
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexLabelNumbering {
+ pub command: Arc<LatexCommand>,
+ pub number: String,
+}
+
+impl LatexLabelNumbering {
+ pub fn name(&self) -> &LatexToken {
+ self.command.extract_word(0).unwrap()
+ }
+
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ commands
+ .iter()
+ .map(Arc::clone)
+ .filter_map(Self::parse_single)
+ .collect()
+ }
+
+ fn parse_single(command: Arc<LatexCommand>) -> Option<Self> {
+ #[derive(Debug, Default)]
+ struct FirstText {
+ text: Option<Arc<LatexText>>,
+ }
+
+ impl LatexVisitor for FirstText {
+ fn visit_root(&mut self, root: Arc<LatexRoot>) {
+ LatexWalker::walk_root(self, root);
+ }
+
+ fn visit_group(&mut self, group: Arc<LatexGroup>) {
+ LatexWalker::walk_group(self, group);
+ }
+
+ fn visit_command(&mut self, command: Arc<LatexCommand>) {
+ LatexWalker::walk_command(self, command);
+ }
+
+ fn visit_text(&mut self, text: Arc<LatexText>) {
+ if self.text.is_none() {
+ self.text = Some(text);
+ }
+ }
+
+ fn visit_comma(&mut self, comma: Arc<LatexComma>) {
+ LatexWalker::walk_comma(self, comma);
+ }
+
+ fn visit_math(&mut self, math: Arc<LatexMath>) {
+ LatexWalker::walk_math(self, math);
+ }
+ }
+
+ if command.name.text() != "\\newlabel" || !command.has_word(0) {
+ return None;
+ }
+
+ let mut analyzer = FirstText::default();
+ analyzer.visit_group(Arc::clone(command.args.get(1)?));
+ let number = analyzer
+ .text?
+ .words
+ .iter()
+ .map(|word| word.text())
+ .join(" ");
+
+ Some(Self {
+ command: Arc::clone(&command),
+ number,
+ })
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexCaption {
+ pub command: Arc<LatexCommand>,
+ pub index: usize,
+}
+
+impl LatexCaption {
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut captions = Vec::new();
+ for command in commands {
+ if command.name.text() == "\\caption" && !command.args.is_empty() {
+ captions.push(Self {
+ command: Arc::clone(&command),
+ index: 0,
+ });
+ }
+ }
+ captions
+ }
+}
+
+impl SyntaxNode for LatexCaption {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexItem {
+ pub command: Arc<LatexCommand>,
+}
+
+impl LatexItem {
+ fn parse(commands: &[Arc<LatexCommand>]) -> Vec<Self> {
+ let mut items = Vec::new();
+ for command in commands {
+ if command.name.text() == "\\item" {
+ items.push(Self {
+ command: Arc::clone(&command),
+ });
+ }
+ }
+ items
+ }
+
+ pub fn name(&self) -> Option<String> {
+ if let Some(options) = self.command.options.get(0) {
+ if options.children.len() == 1 {
+ if let LatexContent::Text(text) = &options.children[0] {
+ return Some(text.words.iter().map(|word| word.text()).join(" "));
+ }
+ }
+ }
+ None
+ }
+}
+
+impl SyntaxNode for LatexItem {
+ fn range(&self) -> Range {
+ self.command.range()
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexStructureInfo {
+ pub sections: Vec<LatexSection>,
+ pub labels: Vec<LatexLabel>,
+ pub label_numberings: Vec<LatexLabelNumbering>,
+ pub captions: Vec<LatexCaption>,
+ pub items: Vec<LatexItem>,
+}
+
+impl LatexStructureInfo {
+ pub fn parse(commands: &[Arc<LatexCommand>]) -> Self {
+ Self {
+ sections: LatexSection::parse(commands),
+ labels: LatexLabel::parse(commands),
+ label_numberings: LatexLabelNumbering::parse(commands),
+ captions: LatexCaption::parse(commands),
+ items: LatexItem::parse(commands),
+ }
+ }
+}