diff options
Diffstat (limited to 'support/texlab/src/syntax/latex/mod.rs')
-rw-r--r-- | support/texlab/src/syntax/latex/mod.rs | 912 |
1 files changed, 620 insertions, 292 deletions
diff --git a/support/texlab/src/syntax/latex/mod.rs b/support/texlab/src/syntax/latex/mod.rs index dc24682752..cee83ab04e 100644 --- a/support/texlab/src/syntax/latex/mod.rs +++ b/support/texlab/src/syntax/latex/mod.rs @@ -1,358 +1,686 @@ +mod analysis; 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>>, + +pub use self::{analysis::*, ast::*}; + +use self::{lexer::Lexer, parser::Parser}; +use crate::{ + protocol::{Options, Uri}, + tex::Resolver, +}; +use std::path::Path; + +#[derive(Debug, PartialEq, Eq, Clone, Copy)] +pub struct OpenParams<'a> { + pub text: &'a str, + pub uri: &'a Uri, + pub resolver: &'a Resolver, + pub options: &'a Options, + pub current_dir: &'a Path, } -impl LatexCommandAnalyzer { - fn parse(root: Arc<LatexRoot>) -> Vec<Arc<LatexCommand>> { - let mut analyzer = Self::default(); - analyzer.visit_root(root); - analyzer.commands - } +pub fn open(params: OpenParams) -> SymbolTable { + let OpenParams { + text, + uri, + resolver, + options, + current_dir, + } = params; + + let lexer = Lexer::new(text); + let parser = Parser::new(lexer); + let tree = parser.parse(); + + let params = SymbolTableParams { + tree, + uri, + resolver, + options, + current_dir, + }; + SymbolTable::analyze(params) } -impl LatexVisitor for LatexCommandAnalyzer { - fn visit_root(&mut self, root: Arc<LatexRoot>) { - LatexWalker::walk_root(self, root); +#[cfg(test)] +mod tests { + use super::*; + use crate::{ + protocol::{Options, Range, RangeExt, Uri}, + syntax::{generic_ast::AstNodeIndex, text::SyntaxNode}, + tex::Resolver, + }; + use indoc::indoc; + use std::env; + + fn open_simple(text: &str) -> SymbolTable { + open(OpenParams { + text: text.trim(), + uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(), + resolver: &Resolver::default(), + options: &Options::default(), + current_dir: &env::current_dir().unwrap(), + }) } - fn visit_group(&mut self, group: Arc<LatexGroup>) { - LatexWalker::walk_group(self, group); + #[derive(Debug, Default)] + struct TreeTraversal { + nodes: Vec<AstNodeIndex>, } - fn visit_command(&mut self, command: Arc<LatexCommand>) { - self.commands.push(Arc::clone(&command)); - LatexWalker::walk_command(self, command); + impl Visitor for TreeTraversal { + fn visit(&mut self, tree: &Tree, node: AstNodeIndex) { + self.nodes.push(node); + tree.walk(self, node); + } } - fn visit_text(&mut self, text: Arc<LatexText>) { - LatexWalker::walk_text(self, text); - } + mod range { + use super::*; - fn visit_comma(&mut self, comma: Arc<LatexComma>) { - LatexWalker::walk_comma(self, comma); - } + fn verify(expected_ranges: Vec<Range>, text: &str) { + let table = open_simple(text); - fn visit_math(&mut self, math: Arc<LatexMath>) { - LatexWalker::walk_math(self, math); - } -} + let mut traversal = TreeTraversal::default(); + traversal.visit(&table.tree, table.tree.root); + let actual_ranges: Vec<_> = traversal + .nodes + .into_iter() + .map(|node| table[node].range()) + .collect(); + assert_eq!(actual_ranges, expected_ranges); + } -#[derive(Debug, PartialEq, Eq, Clone)] -pub struct LatexCitation { - pub command: Arc<LatexCommand>, - pub index: usize, -} + #[test] + fn command() { + verify( + vec![ + Range::new_simple(0, 0, 2, 14), + Range::new_simple(0, 0, 0, 23), + Range::new_simple(0, 14, 0, 23), + Range::new_simple(0, 15, 0, 22), + Range::new_simple(1, 0, 1, 20), + Range::new_simple(1, 11, 1, 20), + Range::new_simple(1, 12, 1, 19), + Range::new_simple(2, 0, 2, 14), + Range::new_simple(2, 4, 2, 9), + Range::new_simple(2, 5, 2, 8), + Range::new_simple(2, 9, 2, 14), + Range::new_simple(2, 10, 2, 13), + ], + indoc!( + r#" + \documentclass{article} + \usepackage{amsmath} + \foo[bar]{baz} + "# + ), + ); + } -impl LatexCitation { - pub fn keys(&self) -> Vec<&LatexToken> { - self.command.extract_comma_separated_words(0) - } + #[test] + fn text() { + verify( + vec![ + Range::new_simple(0, 0, 0, 11), + Range::new_simple(0, 0, 0, 11), + ], + indoc!( + r#" + foo bar baz + "# + ), + ); + } - 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, - }); - } - } + #[test] + fn text_bracket() { + verify( + vec![Range::new_simple(0, 0, 0, 5), Range::new_simple(0, 0, 0, 5)], + indoc!( + r#" + ]foo[ + "# + ), + ); } - citations - } -} -impl SyntaxNode for LatexCitation { - fn range(&self) -> Range { - self.command.range() - } -} + #[test] + fn group() { + verify( + vec![ + Range::new_simple(0, 0, 0, 15), + Range::new_simple(0, 0, 0, 15), + Range::new_simple(0, 2, 0, 5), + Range::new_simple(0, 6, 0, 13), + Range::new_simple(0, 8, 0, 11), + ], + indoc!( + r#" + { foo { bar } } + "# + ), + ); + } -#[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, -} + #[test] + fn group_incomplete() { + verify( + vec![Range::new_simple(0, 1, 0, 2), Range::new_simple(0, 1, 0, 2)], + indoc!( + r#" + }{ + "# + ), + ); + } -impl LatexInclude { - pub fn paths(&self) -> Vec<&LatexToken> { - self.command.extract_comma_separated_words(self.index) + #[test] + fn math() { + verify( + vec![ + Range::new_simple(0, 0, 0, 9), + Range::new_simple(0, 0, 0, 1), + Range::new_simple(0, 2, 0, 7), + Range::new_simple(0, 8, 0, 9), + ], + indoc!( + r#" + $ x = 1 $ + "# + ), + ); + } + + #[test] + fn comma() { + verify( + vec![ + Range::new_simple(0, 0, 0, 8), + Range::new_simple(0, 0, 0, 3), + Range::new_simple(0, 3, 0, 4), + Range::new_simple(0, 5, 0, 8), + ], + indoc!( + r#" + foo, bar + "# + ), + ); + } } - 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 => (), - } + mod command { + use super::*; + + fn verify(expected_names: Vec<&str>, text: &str) { + let table = open(OpenParams { + text, + uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(), + resolver: &Resolver::default(), + options: &Options::default(), + current_dir: &env::current_dir().unwrap(), + }); + + let actual_names: Vec<_> = table + .commands + .iter() + .map(|node| table.tree.as_command(*node).unwrap().name.text()) + .collect(); + + assert_eq!(actual_names, expected_names); + } + + #[test] + fn basic() { + verify( + vec!["\\documentclass", "\\usepackage", "\\begin", "\\end"], + indoc!( + r#" + \documentclass{article} + \usepackage{amsmath} + \begin{document} + Hello World + \end{document} + "# + ), + ); + } + + #[test] + fn star() { + verify( + vec!["\\section*", "\\subsection*"], + indoc!( + r#" + \section*{Foo} + \subsection**{Bar} + "# + ), + ); + } + + #[test] + fn at() { + verify(vec!["\\foo@bar"], indoc!(r#"\foo@bar"#)); + } + + #[test] + fn escape() { + verify(vec!["\\%"], indoc!(r#"\%foo"#)) } - 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); - } - } + mod environment { + use super::*; + + fn verify(expected_names: Vec<(&str, &str)>, text: &str) { + let table = open_simple(text); + let actual_names: Vec<_> = table + .environments + .iter() + .map(|env| { + ( + env.left + .name(&table.tree) + .map(Token::text) + .unwrap_or_default(), + env.right + .name(&table.tree) + .map(Token::text) + .unwrap_or_default(), + ) + }) + .collect(); + + assert_eq!(actual_names, expected_names); + } + + #[test] + fn nested() { + verify( + vec![("b", "b"), ("a", "a")], + indoc!( + r#" + \begin{a} + \begin{b} + \end{b} + \end{a} + "# + ), + ); + } + + #[test] + fn empty_name() { + verify( + vec![("a", ""), ("", "b")], + indoc!( + r#" + \begin{a} + \end{} + \begin{} + \end{b} + "# + ), + ); + } + + #[test] + fn incomplete() { + verify( + Vec::new(), + indoc!( + r#" + \end{a} + \begin{a} + "# + ), + ); + } + + #[test] + fn standalone_true() { + let table = open_simple(r#"\begin{document}\end{document}"#); + assert!(table.is_standalone); + } + + #[test] + fn standalone_false() { + let table = open_simple(r#"\begin{doc}\end{doc}"#); + assert!(!table.is_standalone); } - includes } - fn parse_single( - uri: &Uri, - command: &Arc<LatexCommand>, - description: &LatexIncludeCommand, - ) -> Option<Self> { - if command.name.text() != description.name { - return None; + mod include { + use super::*; + + fn verify(expected_targets: Vec<Vec<&str>>, resolver: Resolver, text: &str) { + let table = open(OpenParams { + text, + uri: &Uri::parse("http://www.foo.com/dir1/dir2/foo.tex").unwrap(), + resolver: &resolver, + options: &Options::default(), + current_dir: &env::current_dir().unwrap(), + }); + + assert_eq!(table.includes.len(), 1); + let include = &table.includes[0]; + let actual_targets: Vec<Vec<&str>> = include + .all_targets + .iter() + .map(|targets| targets.iter().map(|target| target.as_str()).collect()) + .collect(); + + assert_eq!(actual_targets, expected_targets); + } + + #[test] + fn same_directory() { + verify( + vec![vec![ + "http://www.foo.com/dir1/dir2/bar", + "http://www.foo.com/dir1/dir2/bar.tex", + ]], + Resolver::default(), + indoc!(r#"\include{bar}"#), + ); + } + + #[test] + fn two_paths() { + verify( + vec![ + vec![ + "http://www.foo.com/dir1/dir2/bar.tex", + "http://www.foo.com/dir1/dir2/bar.tex.tex", + ], + vec![ + "http://www.foo.com/dir1/dir2/baz.tex", + "http://www.foo.com/dir1/dir2/baz.tex.tex", + ], + ], + Resolver::default(), + indoc!(r#"\input{bar.tex, ./baz.tex}"#), + ); + } + + #[test] + fn sub_directory() { + verify( + vec![vec![ + "http://www.foo.com/dir1/dir2/dir3/bar", + "http://www.foo.com/dir1/dir2/dir3/bar.tex", + ]], + Resolver::default(), + indoc!(r#"\include{dir3/bar}"#), + ); } - if command.args.len() <= description.index { - return None; + #[test] + fn parent_directory() { + verify( + vec![vec![ + "http://www.foo.com/dir1/bar", + "http://www.foo.com/dir1/bar.tex", + ]], + Resolver::default(), + indoc!(r#"\include{../bar}"#), + ); } - 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); + #[test] + fn distro_file() { + let mut resolver = Resolver::default(); + let path = env::current_dir().unwrap().join("biblatex-examples.bib"); + resolver + .files_by_name + .insert("biblatex-examples.bib".into(), path.clone()); + verify( + vec![vec![ + "http://www.foo.com/dir1/dir2/biblatex-examples.bib", + "http://www.foo.com/dir1/dir2/biblatex-examples.bib.bib", + Uri::from_file_path(&path).unwrap().as_str(), + ]], + resolver, + indoc!(r#"\addbibresource{biblatex-examples.bib}"#), + ); } - let include = Self { - command: Arc::clone(command), - index: description.index, - kind: description.kind, - all_targets, - include_extension: description.include_extension, - }; - Some(include) + #[test] + fn component() { + let table = open(OpenParams { + text: indoc!( + r#" + \documentclass{article} + \usepackage{amsmath} + \usepackage{geometry, lipsum} + "# + ), + uri: &Uri::parse("http://www.foo.com/bar.tex").unwrap(), + resolver: &Resolver::default(), + options: &Options::default(), + current_dir: &env::current_dir().unwrap(), + }); + assert_eq!( + table.components, + vec!["article.cls", "amsmath.sty", "geometry.sty", "lipsum.sty"] + ); + } } -} -impl SyntaxNode for LatexInclude { - fn range(&self) -> Range { - self.command.range() + #[test] + fn citation() { + let table = open_simple(indoc!( + r#" + \cite{key1} + \cite{key2, key3} + \nocite{*} + "# + )); + + let expected_keys = vec![vec!["key1"], vec!["key2", "key3"], vec!["*"]]; + + let actual_keys: Vec<Vec<&str>> = table + .citations + .iter() + .map(|cit| cit.keys(&table.tree).into_iter().map(Token::text).collect()) + .collect(); + + assert_eq!(actual_keys, expected_keys); } -} -#[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, -} + #[test] + fn command_definition() { + let table = open_simple(indoc!( + r#" + \newcommand{\foo}{Foo} + \newcommand[2]{\bar}{Bar} + \renewcommand{\baz}{Baz} + \qux + "# + )); + + let expected_cmds = vec!["\\foo", "\\bar", "\\baz"]; + + let actual_cmds: Vec<&str> = table + .command_definitions + .iter() + .map(|def| def.definition_name(&table.tree)) + .collect(); -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 + assert_eq!(actual_cmds, expected_cmds); } -} -impl SyntaxNode for LatexCommandDefinition { - fn range(&self) -> Range { - self.command.range() + #[test] + fn glossary_entry() { + let table = open_simple(indoc!( + r#" + \newglossaryentry{foo}{...} + \newacronym{bar}{...} + "# + )); + + let expected_entries = vec!["foo", "bar"]; + + let actual_entries: Vec<&str> = table + .glossary_entries + .iter() + .map(|entry| entry.label(&table.tree).text()) + .collect(); + + assert_eq!(actual_entries, expected_entries); } -} -#[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, -} + #[test] + fn equation() { + let table = open_simple(indoc!( + r#" + \[ + e^{i \pi} + 1 = 0 + \] + \] \[ + "# + )); + + assert_eq!(table.equations.len(), 1); + } -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, - } + #[test] + fn inline() { + let table = open_simple(indoc!( + r#" + $ x $ + $ + "# + )); + + assert_eq!(table.inlines.len(), 1); } - pub fn find(&self, position: Position) -> Vec<LatexNode> { - let mut finder = LatexFinder::new(position); - finder.visit_root(Arc::clone(&self.root)); - finder.results + #[test] + fn math_operator() { + let table = open_simple(indoc!( + r#" + \DeclareMathOperator{\foo}{foo} + "# + )); + + assert_eq!(table.math_operators.len(), 1); + assert_eq!( + table.math_operators[0].definition_name(&table.tree), + "\\foo" + ); } - 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 + #[test] + fn theorem_definition() { + let table = open_simple(indoc!( + r#" + \newtheorem{lemma}{Lemma} + "# + )); + + assert_eq!(table.theorem_definitions.len(), 1); + assert_eq!( + table.theorem_definitions[0].name(&table.tree).text(), + "lemma" + ); } - 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())) + #[test] + fn section() { + let table = open_simple(indoc!( + r#" + \section{Introduction to \LaTeX} + \subsection*{Foo + "# + )); + assert_eq!(table.sections.len(), 2); + assert_eq!( + table.sections[0].print(&table.tree).unwrap(), + "Introduction to \\LaTeX" + ); + assert_eq!(table.sections[1].print(&table.tree), None); } - pub fn find_label_by_environment(&self, environment: &LatexEnvironment) -> Option<&LatexLabel> { - self.structure + #[test] + fn label() { + let table = open_simple(indoc!( + r#" + \label{foo} + \ref{bar, baz} + "# + )); + + let expected_names = vec![vec!["foo"], vec!["bar", "baz"]]; + + let actual_names: Vec<Vec<&str>> = table .labels .iter() - .filter(|label| label.kind == LatexLabelKind::Definition) - .filter(|label| label.names().len() == 1) - .find(|label| self.is_direct_child(environment, label.start())) + .map(|label| { + label + .names(&table.tree) + .into_iter() + .map(Token::text) + .collect() + }) + .collect(); + + assert_eq!(actual_names, expected_names); } - 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())) + #[test] + fn label_numbering() { + let table = open_simple(indoc!( + r#" + \newlabel{foo}{{1}{1}} + "# + )); + + assert_eq!(table.label_numberings.len(), 1); + assert_eq!(table.label_numberings[0].name(&table.tree).text(), "foo"); + assert_eq!(table.label_numberings[0].number, "1"); } - 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)) + #[test] + fn caption() { + let table = open_simple(indoc!( + r#" + \caption{Foo \LaTeX Bar} + "# + )); + + assert_eq!(table.captions.len(), 1); + assert_eq!( + table.captions[0].print(&table.tree).unwrap(), + "Foo \\LaTeX Bar" + ); } -} -pub fn extract_group(content: &LatexGroup) -> String { - if content.children.is_empty() || content.right.is_none() { - return String::new(); + #[test] + fn item_without_name() { + let table = open_simple(indoc!( + r#" + \item + "# + )); + + assert_eq!(table.items.len(), 1); + assert_eq!(table.items[0].name(&table.tree), None); } - let mut printer = LatexPrinter::new(content.children[0].start()); - for child in &content.children { - child.accept(&mut printer); + #[test] + fn item_with_name() { + let table = open_simple(indoc!( + r#" + \item[foo bar] + "# + )); + + assert_eq!(table.items.len(), 1); + assert_eq!(table.items[0].name(&table.tree).unwrap(), "foo bar"); } - printer.output } |