summaryrefslogtreecommitdiff
path: root/support/texlab/src/symbol
diff options
context:
space:
mode:
Diffstat (limited to 'support/texlab/src/symbol')
-rw-r--r--support/texlab/src/symbol/bibtex_entry.rs147
-rw-r--r--support/texlab/src/symbol/bibtex_string.rs79
-rw-r--r--support/texlab/src/symbol/latex_section/enumeration.rs85
-rw-r--r--support/texlab/src/symbol/latex_section/equation.rs41
-rw-r--r--support/texlab/src/symbol/latex_section/float.rs57
-rw-r--r--support/texlab/src/symbol/latex_section/mod.rs634
-rw-r--r--support/texlab/src/symbol/latex_section/theorem.rs66
-rw-r--r--support/texlab/src/symbol/mod.rs261
-rw-r--r--support/texlab/src/symbol/project_order.rs142
9 files changed, 1512 insertions, 0 deletions
diff --git a/support/texlab/src/symbol/bibtex_entry.rs b/support/texlab/src/symbol/bibtex_entry.rs
new file mode 100644
index 0000000000..f19c2fb734
--- /dev/null
+++ b/support/texlab/src/symbol/bibtex_entry.rs
@@ -0,0 +1,147 @@
+use super::{LatexSymbol, LatexSymbolKind};
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::*;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct BibtexEntrySymbolProvider;
+
+impl FeatureProvider for BibtexEntrySymbolProvider {
+ type Params = DocumentSymbolParams;
+ type Output = Vec<LatexSymbol>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let mut symbols = Vec::new();
+ if let SyntaxTree::Bibtex(tree) = &request.document().tree {
+ for entry in tree
+ .entries()
+ .iter()
+ .filter(|entry| !entry.is_comment())
+ .filter(|entry| entry.key.is_some())
+ {
+ let category = LANGUAGE_DATA
+ .find_entry_type(&entry.ty.text()[1..])
+ .map(|ty| ty.category)
+ .unwrap_or(BibtexEntryTypeCategory::Misc);
+
+ let key = entry.key.as_ref().unwrap();
+ let symbol = LatexSymbol {
+ name: key.text().to_owned(),
+ label: None,
+ kind: LatexSymbolKind::Entry(category),
+ deprecated: false,
+ full_range: entry.range(),
+ selection_range: key.range(),
+ children: Self::field_symbols(&entry),
+ };
+ symbols.push(symbol);
+ }
+ }
+ symbols
+ }
+}
+
+impl BibtexEntrySymbolProvider {
+ fn field_symbols(entry: &BibtexEntry) -> Vec<LatexSymbol> {
+ let mut symbols = Vec::new();
+ for field in &entry.fields {
+ let symbol = LatexSymbol {
+ name: field.name.text().to_owned(),
+ label: None,
+ kind: LatexSymbolKind::Field,
+ deprecated: false,
+ full_range: field.range(),
+ selection_range: field.name.range(),
+ children: Vec::new(),
+ };
+ symbols.push(symbol);
+ }
+ symbols
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+
+ #[test]
+ fn test_entry() {
+ let symbols = test_feature(
+ BibtexEntrySymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.bib",
+ "@article{key, foo = bar, baz = qux}",
+ )],
+ main_file: "foo.bib",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "key".into(),
+ label: None,
+ kind: LatexSymbolKind::Entry(BibtexEntryTypeCategory::Article),
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 0, 35),
+ selection_range: Range::new_simple(0, 9, 0, 12),
+ children: vec![
+ LatexSymbol {
+ name: "foo".into(),
+ label: None,
+ kind: LatexSymbolKind::Field,
+ deprecated: false,
+ full_range: Range::new_simple(0, 14, 0, 24),
+ selection_range: Range::new_simple(0, 14, 0, 17),
+ children: Vec::new(),
+ },
+ LatexSymbol {
+ name: "baz".into(),
+ label: None,
+ kind: LatexSymbolKind::Field,
+ deprecated: false,
+ full_range: Range::new_simple(0, 25, 0, 34),
+ selection_range: Range::new_simple(0, 25, 0, 28),
+ children: Vec::new(),
+ },
+ ],
+ }]
+ );
+ }
+
+ #[test]
+ fn test_comment() {
+ let symbols = test_feature(
+ BibtexEntrySymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.bib",
+ "@comment{key, foo = bar, baz = qux}",
+ )],
+ main_file: "foo.bib",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(symbols, Vec::new());
+ }
+
+ #[test]
+ fn test_latex() {
+ let symbols = test_feature(
+ BibtexEntrySymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "@article{key, foo = bar, baz = qux}",
+ )],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(symbols, Vec::new());
+ }
+}
diff --git a/support/texlab/src/symbol/bibtex_string.rs b/support/texlab/src/symbol/bibtex_string.rs
new file mode 100644
index 0000000000..8f1f436048
--- /dev/null
+++ b/support/texlab/src/symbol/bibtex_string.rs
@@ -0,0 +1,79 @@
+use super::{LatexSymbol, LatexSymbolKind};
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::*;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct BibtexStringSymbolProvider;
+
+impl FeatureProvider for BibtexStringSymbolProvider {
+ type Params = DocumentSymbolParams;
+ type Output = Vec<LatexSymbol>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let mut symbols = Vec::new();
+ if let SyntaxTree::Bibtex(tree) = &request.document().tree {
+ for child in &tree.root.children {
+ if let BibtexDeclaration::String(string) = &child {
+ if let Some(name) = &string.name {
+ symbols.push(LatexSymbol {
+ name: name.text().to_owned(),
+ label: None,
+ kind: LatexSymbolKind::String,
+ deprecated: false,
+ full_range: string.range(),
+ selection_range: name.range(),
+ children: Vec::new(),
+ });
+ }
+ }
+ }
+ }
+ symbols
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+
+ #[test]
+ fn test_valid() {
+ let symbols = test_feature(
+ BibtexStringSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.bib", "@string{key = \"value\"}")],
+ main_file: "foo.bib",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "key".into(),
+ label: None,
+ kind: LatexSymbolKind::String,
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 0, 22),
+ selection_range: Range::new_simple(0, 8, 0, 11),
+ children: Vec::new(),
+ }]
+ );
+ }
+
+ #[test]
+ fn test_invalid() {
+ let symbols = test_feature(
+ BibtexStringSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file("foo.bib", "@string{}")],
+ main_file: "foo.bib",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(symbols, Vec::new());
+ }
+}
diff --git a/support/texlab/src/symbol/latex_section/enumeration.rs b/support/texlab/src/symbol/latex_section/enumeration.rs
new file mode 100644
index 0000000000..34f4dff72f
--- /dev/null
+++ b/support/texlab/src/symbol/latex_section/enumeration.rs
@@ -0,0 +1,85 @@
+use super::{label_name, selection_range};
+use crate::range::RangeExt;
+use crate::symbol::{LatexSymbol, LatexSymbolKind};
+use crate::syntax::*;
+use crate::workspace::*;
+use lsp_types::Range;
+
+pub fn symbols(view: &DocumentView, tree: &LatexSyntaxTree) -> Vec<LatexSymbol> {
+ let mut symbols = Vec::new();
+ for environment in &tree.env.environments {
+ if environment.left.is_enum() {
+ symbols.push(make_symbol(view, tree, environment));
+ }
+ }
+ symbols
+}
+
+fn make_symbol(
+ view: &DocumentView,
+ tree: &LatexSyntaxTree,
+ enumeration: &LatexEnvironment,
+) -> LatexSymbol {
+ let name = titlelize(enumeration.left.name().unwrap().text());
+
+ let items: Vec<_> = tree
+ .structure
+ .items
+ .iter()
+ .filter(|item| tree.is_enumeration_item(enumeration, item))
+ .collect();
+
+ let mut children = Vec::new();
+ for i in 0..items.len() {
+ let start = items[i].start();
+ let end = items
+ .get(i + 1)
+ .map(|item| item.start())
+ .unwrap_or_else(|| enumeration.right.start());
+ let range = Range::new(start, end);
+
+ let label = find_item_label(tree, range);
+
+ let number = items[i].name().or_else(|| {
+ label
+ .as_ref()
+ .and_then(|label| OutlineContext::find_number(view, label))
+ });
+
+ let name = number.unwrap_or_else(|| "Item".into());
+ children.push(LatexSymbol {
+ name,
+ label: label_name(label),
+ kind: LatexSymbolKind::EnumerationItem,
+ deprecated: false,
+ full_range: range,
+ selection_range: selection_range(items[i].range(), label),
+ children: Vec::new(),
+ });
+ }
+
+ LatexSymbol {
+ name,
+ label: None,
+ kind: LatexSymbolKind::Enumeration,
+ deprecated: false,
+ full_range: enumeration.range(),
+ selection_range: enumeration.range(),
+ children,
+ }
+}
+
+fn find_item_label(tree: &LatexSyntaxTree, item_range: Range) -> Option<&LatexLabel> {
+ let label = tree.find_label_by_range(item_range)?;
+ if tree
+ .env
+ .environments
+ .iter()
+ .filter(|env| item_range.contains(env.start()))
+ .all(|env| !env.range().contains(label.start()))
+ {
+ Some(label)
+ } else {
+ None
+ }
+}
diff --git a/support/texlab/src/symbol/latex_section/equation.rs b/support/texlab/src/symbol/latex_section/equation.rs
new file mode 100644
index 0000000000..4c2fcbe295
--- /dev/null
+++ b/support/texlab/src/symbol/latex_section/equation.rs
@@ -0,0 +1,41 @@
+use super::{label_name, selection_range};
+use crate::symbol::{LatexSymbol, LatexSymbolKind};
+use crate::syntax::*;
+use crate::workspace::*;
+use lsp_types::Range;
+
+pub fn symbols(view: &DocumentView, tree: &LatexSyntaxTree) -> Vec<LatexSymbol> {
+ let mut symbols = Vec::new();
+ for equation in &tree.math.equations {
+ symbols.push(make_symbol(view, tree, equation.range()));
+ }
+
+ for equation in &tree.env.environments {
+ if equation.left.is_math() {
+ symbols.push(make_symbol(view, tree, equation.range()));
+ }
+ }
+ symbols
+}
+
+fn make_symbol(view: &DocumentView, tree: &LatexSyntaxTree, full_range: Range) -> LatexSymbol {
+ let label = tree.find_label_by_range(full_range);
+
+ let name = match label
+ .as_ref()
+ .and_then(|label| OutlineContext::find_number(view, label))
+ {
+ Some(num) => format!("Equation ({})", num),
+ None => "Equation".to_owned(),
+ };
+
+ LatexSymbol {
+ name,
+ label: label_name(label),
+ kind: LatexSymbolKind::Equation,
+ deprecated: false,
+ full_range,
+ selection_range: selection_range(full_range, label),
+ children: Vec::new(),
+ }
+}
diff --git a/support/texlab/src/symbol/latex_section/float.rs b/support/texlab/src/symbol/latex_section/float.rs
new file mode 100644
index 0000000000..9356d0ed3e
--- /dev/null
+++ b/support/texlab/src/symbol/latex_section/float.rs
@@ -0,0 +1,57 @@
+use super::{label_name, selection_range};
+use crate::symbol::{LatexSymbol, LatexSymbolKind};
+use crate::syntax::*;
+use crate::workspace::*;
+
+pub fn symbols(view: &DocumentView, tree: &LatexSyntaxTree) -> Vec<LatexSymbol> {
+ tree.structure
+ .captions
+ .iter()
+ .filter_map(|caption| make_symbol(view, tree, caption))
+ .collect()
+}
+
+fn make_symbol(
+ view: &DocumentView,
+ tree: &LatexSyntaxTree,
+ caption: &LatexCaption,
+) -> Option<LatexSymbol> {
+ let environment = tree
+ .env
+ .environments
+ .iter()
+ .find(|env| tree.is_direct_child(env, caption.start()))?;
+ let text = extract_group(&caption.command.args[caption.index]);
+
+ let kind = environment
+ .left
+ .name()
+ .map(LatexToken::text)
+ .and_then(OutlineCaptionKind::parse)?;
+
+ let label = tree.find_label_by_environment(environment);
+ let number = label
+ .as_ref()
+ .and_then(|label| OutlineContext::find_number(view, label));
+
+ let name = match &number {
+ Some(number) => format!("{} {}: {}", kind.as_str(), number, text),
+ None => format!("{}: {}", kind.as_str(), text),
+ };
+
+ let symbol = LatexSymbol {
+ name,
+ label: label_name(label),
+ kind: match kind {
+ OutlineCaptionKind::Figure => LatexSymbolKind::Figure,
+ OutlineCaptionKind::Table => LatexSymbolKind::Table,
+ OutlineCaptionKind::Listing => LatexSymbolKind::Listing,
+ OutlineCaptionKind::Algorithm => LatexSymbolKind::Algorithm,
+ },
+ deprecated: false,
+ full_range: environment.range(),
+ selection_range: selection_range(environment.range(), label),
+ children: Vec::new(),
+ };
+ Some(symbol)
+}
diff --git a/support/texlab/src/symbol/latex_section/mod.rs b/support/texlab/src/symbol/latex_section/mod.rs
new file mode 100644
index 0000000000..f6fe2aede9
--- /dev/null
+++ b/support/texlab/src/symbol/latex_section/mod.rs
@@ -0,0 +1,634 @@
+mod enumeration;
+mod equation;
+mod float;
+mod theorem;
+
+use super::{LatexSymbol, LatexSymbolKind};
+use crate::range::RangeExt;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::*;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub struct LatexSectionSymbolProvider;
+
+impl FeatureProvider for LatexSectionSymbolProvider {
+ type Params = DocumentSymbolParams;
+ type Output = Vec<LatexSymbol>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ let mut symbols = Vec::new();
+ if let SyntaxTree::Latex(tree) = &request.document().tree {
+ let mut section_tree = build_section_tree(&request.view, tree);
+ for symbol in enumeration::symbols(&request.view, tree) {
+ section_tree.insert_symbol(&symbol);
+ }
+
+ for symbol in equation::symbols(&request.view, tree) {
+ section_tree.insert_symbol(&symbol);
+ }
+
+ for symbol in float::symbols(&request.view, tree) {
+ section_tree.insert_symbol(&symbol);
+ }
+
+ for symbol in theorem::symbols(&request.view, tree) {
+ section_tree.insert_symbol(&symbol);
+ }
+
+ for symbol in section_tree.symbols {
+ symbols.push(symbol);
+ }
+
+ for child in section_tree.children {
+ symbols.push(child.into());
+ }
+ }
+ symbols
+ }
+}
+
+pub fn build_section_tree<'a>(
+ view: &'a DocumentView,
+ tree: &'a LatexSyntaxTree,
+) -> LatexSectionTree<'a> {
+ let mut section_tree = LatexSectionTree::from(tree);
+ section_tree.set_full_text(&view.document.text);
+ let end_position = compute_end_position(tree, &view.document.text);
+ LatexSectionNode::set_full_range(&mut section_tree.children, end_position);
+ let outline = Outline::from(view);
+ for child in &mut section_tree.children {
+ child.set_label(tree, view, &outline);
+ }
+ section_tree
+}
+
+fn compute_end_position(tree: &LatexSyntaxTree, text: &str) -> Position {
+ let mut stream = CharStream::new(text);
+ while stream.next().is_some() {}
+ tree.env
+ .environments
+ .iter()
+ .find(|env| env.left.name().map(LatexToken::text) == Some("document"))
+ .map(|env| env.right.start())
+ .unwrap_or(stream.current_position)
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexSectionNode<'a> {
+ pub section: &'a LatexSection,
+ pub full_range: Range,
+ full_text: &'a str,
+ label: Option<String>,
+ number: Option<String>,
+ symbols: Vec<LatexSymbol>,
+ children: Vec<Self>,
+}
+
+impl<'a> LatexSectionNode<'a> {
+ fn new(section: &'a LatexSection) -> Self {
+ Self {
+ section,
+ full_range: Range::default(),
+ full_text: "",
+ label: None,
+ number: None,
+ symbols: Vec::new(),
+ children: Vec::new(),
+ }
+ }
+
+ fn set_full_text(&mut self, full_text: &'a str) {
+ self.full_text = full_text;
+ for child in &mut self.children {
+ child.set_full_text(full_text);
+ }
+ }
+
+ fn name(&self) -> String {
+ self.section
+ .extract_text(self.full_text)
+ .unwrap_or_else(|| "Unknown".to_owned())
+ }
+
+ fn set_full_range(children: &mut Vec<Self>, end_position: Position) {
+ for i in 0..children.len() {
+ let current_end = children
+ .get(i + 1)
+ .map(|next| next.section.start())
+ .unwrap_or(end_position);
+
+ let mut current = &mut children[i];
+ current.full_range = Range::new(current.section.start(), current_end);
+ Self::set_full_range(&mut current.children, current_end);
+ }
+ }
+
+ fn set_label(&mut self, tree: &LatexSyntaxTree, view: &DocumentView, outline: &Outline) {
+ if let Some(label) = tree
+ .structure
+ .labels
+ .iter()
+ .filter(|label| label.kind == LatexLabelKind::Definition)
+ .find(|label| self.full_range.contains(label.start()))
+ {
+ if let Some(ctx) = OutlineContext::parse(view, label, outline) {
+ let mut is_section = false;
+ if let OutlineContextItem::Section { text, .. } = &ctx.item {
+ if self.name() == *text {
+ for name in label.names() {
+ self.label = Some(name.text().to_owned());
+ }
+
+ is_section = true;
+ }
+ }
+
+ if is_section {
+ self.number = ctx.number;
+ }
+ }
+ }
+
+ for child in &mut self.children {
+ child.set_label(tree, view, outline);
+ }
+ }
+
+ fn insert_section(nodes: &mut Vec<Self>, section: &'a LatexSection) {
+ match nodes.last_mut() {
+ Some(parent) => {
+ if parent.section.level < section.level {
+ Self::insert_section(&mut parent.children, section);
+ } else {
+ nodes.push(LatexSectionNode::new(section));
+ }
+ }
+ None => {
+ nodes.push(LatexSectionNode::new(section));
+ }
+ }
+ }
+
+ fn insert_symbol(&mut self, symbol: &LatexSymbol) -> bool {
+ if !self.full_range.contains(symbol.selection_range.start) {
+ return false;
+ }
+
+ for child in &mut self.children {
+ if child.insert_symbol(symbol) {
+ return true;
+ }
+ }
+
+ self.symbols.push(symbol.clone());
+ true
+ }
+
+ fn find(&self, label: &str) -> Option<&Self> {
+ if self.label.as_ref().map(AsRef::as_ref) == Some(label) {
+ Some(self)
+ } else {
+ for child in &self.children {
+ let result = child.find(label);
+ if result.is_some() {
+ return result;
+ }
+ }
+ None
+ }
+ }
+}
+
+impl<'a> Into<LatexSymbol> for LatexSectionNode<'a> {
+ fn into(self) -> LatexSymbol {
+ let name = self.name();
+
+ let mut children: Vec<LatexSymbol> = self.children.into_iter().map(Into::into).collect();
+
+ for symbol in self.symbols {
+ children.push(symbol);
+ }
+
+ let full_name = match &self.number {
+ Some(number) => format!("{} {}", number, name),
+ None => name,
+ };
+
+ LatexSymbol {
+ name: full_name,
+ label: self.label,
+ kind: LatexSymbolKind::Section,
+ deprecated: false,
+ full_range: self.full_range,
+ selection_range: self.section.range(),
+ children,
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexSectionTree<'a> {
+ symbols: Vec<LatexSymbol>,
+ children: Vec<LatexSectionNode<'a>>,
+}
+
+impl<'a> LatexSectionTree<'a> {
+ fn new() -> Self {
+ Self {
+ symbols: Vec::new(),
+ children: Vec::new(),
+ }
+ }
+
+ fn set_full_text(&mut self, full_text: &'a str) {
+ for child in &mut self.children {
+ child.set_full_text(full_text);
+ }
+ }
+
+ fn insert_symbol(&mut self, symbol: &LatexSymbol) {
+ for child in &mut self.children {
+ if child.insert_symbol(symbol) {
+ return;
+ }
+ }
+ self.symbols.push(symbol.clone());
+ }
+
+ pub fn find(&self, label: &str) -> Option<&LatexSectionNode<'a>> {
+ for child in &self.children {
+ let result = child.find(label);
+ if result.is_some() {
+ return result;
+ }
+ }
+ None
+ }
+}
+
+impl<'a> From<&'a LatexSyntaxTree> for LatexSectionTree<'a> {
+ fn from(tree: &'a LatexSyntaxTree) -> Self {
+ let mut root = Self::new();
+ for section in &tree.structure.sections {
+ LatexSectionNode::insert_section(&mut root.children, section);
+ }
+ root
+ }
+}
+
+pub fn label_name(label: Option<&LatexLabel>) -> Option<String> {
+ label.map(|label| label.names()[0].text().to_owned())
+}
+
+pub fn selection_range(full_range: Range, label: Option<&LatexLabel>) -> Range {
+ label.map(|label| label.range()).unwrap_or(full_range)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::range::RangeExt;
+
+ #[test]
+ fn test_subsection() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file(
+ "foo.tex",
+ "\\section{Foo}\n\\subsection{Bar}\\label{sec:bar}\n\\subsection{Baz}\n\\section{Qux}",
+ ),
+ FeatureSpec::file(
+ "foo.aux",
+ "\\newlabel{sec:bar}{{\\relax 2.1}{4}{Bar\\relax }{figure.caption.4}{}}"
+ ),
+ ],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![
+ LatexSymbol {
+ name: "Foo".into(),
+ label: None,
+ kind: LatexSymbolKind::Section,
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 3, 0),
+ selection_range: Range::new_simple(0, 0, 0, 13),
+ children: vec![
+ LatexSymbol {
+ name: "2.1 Bar".into(),
+ label: Some("sec:bar".into()),
+ kind: LatexSymbolKind::Section,
+ deprecated: false,
+ full_range: Range::new_simple(1, 0, 2, 0),
+ selection_range: Range::new_simple(1, 0, 1, 16),
+ children: Vec::new(),
+ },
+ LatexSymbol {
+ name: "Baz".into(),
+ label: None,
+ kind: LatexSymbolKind::Section,
+ deprecated: false,
+ full_range: Range::new_simple(2, 0, 3, 0),
+ selection_range: Range::new_simple(2, 0, 2, 16),
+ children: Vec::new(),
+ },
+ ],
+ },
+ LatexSymbol {
+ name: "Qux".into(),
+ label: None,
+ kind: LatexSymbolKind::Section,
+ deprecated: false,
+ full_range: Range::new_simple(3, 0, 3, 13),
+ selection_range: Range::new_simple(3, 0, 3, 13),
+ children: Vec::new(),
+ }
+ ]
+ );
+ }
+
+ #[test]
+ fn test_section_inside_document_environment() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\begin{document}\\section{Foo}\\relax\n\\end{document}",
+ )],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Foo".into(),
+ label: None,
+ kind: LatexSymbolKind::Section,
+ deprecated: false,
+ full_range: Range::new_simple(0, 16, 1, 0),
+ selection_range: Range::new_simple(0, 16, 0, 29),
+ children: Vec::new()
+ }]
+ );
+ }
+
+ #[test]
+ fn test_enumeration() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\section{Foo}\n\\begin{enumerate}\n\\end{enumerate}",
+ )],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Foo".into(),
+ label: None,
+ kind: LatexSymbolKind::Section,
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 2, 15),
+ selection_range: Range::new_simple(0, 0, 0, 13),
+ children: vec![LatexSymbol {
+ name: "Enumerate".into(),
+ label: None,
+ kind: LatexSymbolKind::Enumeration,
+ deprecated: false,
+ full_range: Range::new_simple(1, 0, 2, 15),
+ selection_range: Range::new_simple(1, 0, 2, 15),
+ children: Vec::new(),
+ },],
+ },]
+ );
+ }
+
+ #[test]
+ fn test_equation() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\[Foo\\]\n\\begin{equation}\\label{eq:foo}\\end{equation}",
+ )],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![
+ LatexSymbol {
+ name: "Equation".into(),
+ label: None,
+ kind: LatexSymbolKind::Equation,
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 0, 7),
+ selection_range: Range::new_simple(0, 0, 0, 7),
+ children: Vec::new(),
+ },
+ LatexSymbol {
+ name: "Equation".into(),
+ label: Some("eq:foo".into()),
+ kind: LatexSymbolKind::Equation,
+ deprecated: false,
+ full_range: Range::new_simple(1, 0, 1, 44),
+ selection_range: Range::new_simple(1, 16, 1, 30),
+ children: Vec::new(),
+ },
+ ]
+ );
+ }
+
+ #[test]
+ fn test_equation_number() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file("foo.tex", "\\[\\label{eq:foo}\\]"),
+ FeatureSpec::file(
+ "foo.aux",
+ "\\newlabel{eq:foo}{{\\relax 2.1}{4}{Bar\\relax }{figure.caption.4}{}}",
+ ),
+ ],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Equation (2.1)".into(),
+ label: Some("eq:foo".into()),
+ kind: LatexSymbolKind::Equation,
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 0, 18),
+ selection_range: Range::new_simple(0, 2, 0, 16),
+ children: Vec::new(),
+ },]
+ );
+ }
+
+ #[test]
+ fn test_table() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\begin{table}\\caption{Foo}\\end{table}",
+ )],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Table: Foo".into(),
+ label: None,
+ kind: LatexSymbolKind::Table,
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 0, 37),
+ selection_range: Range::new_simple(0, 0, 0, 37),
+ children: Vec::new(),
+ },]
+ );
+ }
+
+ #[test]
+ fn test_figure_number() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file(
+ "foo.tex",
+ "\\begin{figure}\\caption{Foo}\\label{fig:foo}\\end{figure}",
+ ),
+ FeatureSpec::file(
+ "foo.aux",
+ "\\newlabel{fig:foo}{{\\relax 2.1}{4}{Bar\\relax }{figure.caption.4}{}}",
+ ),
+ ],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Figure 2.1: Foo".into(),
+ label: Some("fig:foo".into()),
+ kind: LatexSymbolKind::Figure,
+ deprecated: false,
+ full_range: Range::new_simple(0, 0, 0, 54),
+ selection_range: Range::new_simple(0, 27, 0, 42),
+ children: Vec::new(),
+ },]
+ );
+ }
+
+ #[test]
+ fn test_lemma() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\newtheorem{lemma}{Lemma}\\begin{lemma}\\end{lemma}",
+ )],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Lemma".into(),
+ label: None,
+ kind: LatexSymbolKind::Theorem,
+ deprecated: false,
+ full_range: Range::new_simple(0, 25, 0, 49),
+ selection_range: Range::new_simple(0, 25, 0, 49),
+ children: Vec::new(),
+ },]
+ );
+ }
+
+ #[test]
+ fn test_lemma_number() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![
+ FeatureSpec::file(
+ "foo.tex",
+ "\\newtheorem{lemma}{Lemma}\n\\begin{lemma}\\label{thm:foo}\\end{lemma}",
+ ),
+ FeatureSpec::file(
+ "foo.aux",
+ "\\newlabel{thm:foo}{{\\relax 2.1}{4}{Bar\\relax }{figure.caption.4}{}}",
+ ),
+ ],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Lemma 2.1".into(),
+ label: Some("thm:foo".into()),
+ kind: LatexSymbolKind::Theorem,
+ deprecated: false,
+ full_range: Range::new_simple(1, 0, 1, 39),
+ selection_range: Range::new_simple(1, 13, 1, 28),
+ children: Vec::new(),
+ },]
+ );
+ }
+
+ #[test]
+ fn test_lemma_description() {
+ let symbols = test_feature(
+ LatexSectionSymbolProvider,
+ FeatureSpec {
+ files: vec![FeatureSpec::file(
+ "foo.tex",
+ "\\newtheorem{lemma}{Lemma}\\begin{lemma}[Foo]\\end{lemma}",
+ )],
+ main_file: "foo.tex",
+ ..FeatureSpec::default()
+ },
+ );
+ assert_eq!(
+ symbols,
+ vec![LatexSymbol {
+ name: "Lemma (Foo)".into(),
+ label: None,
+ kind: LatexSymbolKind::Theorem,
+ deprecated: false,
+ full_range: Range::new_simple(0, 25, 0, 54),
+ selection_range: Range::new_simple(0, 25, 0, 54),
+ children: Vec::new(),
+ },]
+ );
+ }
+}
diff --git a/support/texlab/src/symbol/latex_section/theorem.rs b/support/texlab/src/symbol/latex_section/theorem.rs
new file mode 100644
index 0000000000..1f2190ea47
--- /dev/null
+++ b/support/texlab/src/symbol/latex_section/theorem.rs
@@ -0,0 +1,66 @@
+use super::{label_name, selection_range};
+use crate::symbol::{LatexSymbol, LatexSymbolKind};
+use crate::syntax::*;
+use crate::workspace::*;
+
+pub fn symbols(view: &DocumentView, tree: &LatexSyntaxTree) -> Vec<LatexSymbol> {
+ tree.env
+ .environments
+ .iter()
+ .filter_map(|env| make_symbol(view, tree, env))
+ .collect()
+}
+
+fn make_symbol(
+ view: &DocumentView,
+ main_tree: &LatexSyntaxTree,
+ environment: &LatexEnvironment,
+) -> Option<LatexSymbol> {
+ let environment_name = environment.left.name().map(LatexToken::text)?;
+
+ for document in &view.related_documents {
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ for definition in &tree.math.theorem_definitions {
+ if environment_name == definition.name().text() {
+ let kind = definition
+ .command
+ .args
+ .get(definition.index + 1)
+ .map(|content| extract_group(content))
+ .unwrap_or_else(|| titlelize(environment_name));
+
+ let description = environment
+ .left
+ .command
+ .options
+ .get(0)
+ .map(|content| extract_group(content));
+
+ let label = main_tree.find_label_by_environment(environment);
+ let number = label
+ .as_ref()
+ .and_then(|label| OutlineContext::find_number(view, label));
+
+ let name = match (description, number) {
+ (Some(desc), Some(num)) => format!("{} {} ({})", kind, num, desc),
+ (Some(desc), None) => format!("{} ({})", kind, desc),
+ (None, Some(num)) => format!("{} {}", kind, num),
+ (None, None) => kind,
+ };
+
+ let symbol = LatexSymbol {
+ name,
+ label: label_name(label),
+ kind: LatexSymbolKind::Theorem,
+ deprecated: false,
+ full_range: environment.range(),
+ selection_range: selection_range(environment.range(), label),
+ children: Vec::new(),
+ };
+ return Some(symbol);
+ }
+ }
+ }
+ }
+ None
+}
diff --git a/support/texlab/src/symbol/mod.rs b/support/texlab/src/symbol/mod.rs
new file mode 100644
index 0000000000..ec68d70c3e
--- /dev/null
+++ b/support/texlab/src/symbol/mod.rs
@@ -0,0 +1,261 @@
+mod bibtex_entry;
+mod bibtex_string;
+mod latex_section;
+mod project_order;
+
+use self::bibtex_entry::BibtexEntrySymbolProvider;
+use self::bibtex_string::BibtexStringSymbolProvider;
+use self::latex_section::LatexSectionSymbolProvider;
+use self::project_order::ProjectOrdering;
+use crate::capabilities::ClientCapabilitiesExt;
+use crate::lsp_kind::Structure;
+use crate::syntax::*;
+use crate::workspace::*;
+use futures_boxed::boxed;
+use lsp_types::*;
+use serde::{Deserialize, Serialize};
+use std::cmp::Reverse;
+use std::sync::Arc;
+
+pub use self::latex_section::{build_section_tree, LatexSectionNode, LatexSectionTree};
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum LatexSymbolKind {
+ Section,
+ Figure,
+ Algorithm,
+ Table,
+ Listing,
+ Enumeration,
+ EnumerationItem,
+ Theorem,
+ Equation,
+ Entry(BibtexEntryTypeCategory),
+ Field,
+ String,
+}
+
+impl Into<SymbolKind> for LatexSymbolKind {
+ fn into(self) -> SymbolKind {
+ match self {
+ Self::Section => Structure::Section.symbol_kind(),
+ Self::Figure | Self::Algorithm | Self::Table | Self::Listing => {
+ Structure::Float.symbol_kind()
+ }
+ Self::Enumeration => Structure::Environment.symbol_kind(),
+ Self::EnumerationItem => Structure::Item.symbol_kind(),
+ Self::Theorem => Structure::Theorem.symbol_kind(),
+ Self::Equation => Structure::Equation.symbol_kind(),
+ Self::Entry(category) => Structure::Entry(category).symbol_kind(),
+ Self::Field => Structure::Field.symbol_kind(),
+ Self::String => Structure::Entry(BibtexEntryTypeCategory::String).symbol_kind(),
+ }
+ }
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct LatexSymbol {
+ pub name: String,
+ pub label: Option<String>,
+ pub kind: LatexSymbolKind,
+ pub deprecated: bool,
+ pub full_range: Range,
+ pub selection_range: Range,
+ pub children: Vec<LatexSymbol>,
+}
+
+impl LatexSymbol {
+ pub fn search_text(&self) -> String {
+ let kind = match self.kind {
+ LatexSymbolKind::Section => "latex section",
+ LatexSymbolKind::Figure => "latex float figure",
+ LatexSymbolKind::Algorithm => "latex float algorithm",
+ LatexSymbolKind::Table => "latex float table",
+ LatexSymbolKind::Listing => "latex float listing",
+ LatexSymbolKind::Enumeration => "latex enumeration",
+ LatexSymbolKind::EnumerationItem => "latex enumeration item",
+ LatexSymbolKind::Theorem => "latex math",
+ LatexSymbolKind::Equation => "latex math equation",
+ LatexSymbolKind::Entry(_) => "bibtex entry",
+ LatexSymbolKind::Field => "bibtex field",
+ LatexSymbolKind::String => "bibtex string",
+ };
+ format!("{} {}", kind, self.name).to_lowercase()
+ }
+
+ pub fn flatten(mut self, buffer: &mut Vec<Self>) {
+ if self.kind == LatexSymbolKind::Field {
+ return;
+ }
+ for symbol in self.children.drain(..) {
+ symbol.flatten(buffer);
+ }
+ buffer.push(self);
+ }
+
+ pub fn into_symbol_info(self, uri: Uri) -> SymbolInformation {
+ SymbolInformation {
+ name: self.name,
+ deprecated: Some(self.deprecated),
+ kind: self.kind.into(),
+ container_name: None,
+ location: Location::new(uri.clone().into(), self.full_range),
+ }
+ }
+}
+
+impl Into<DocumentSymbol> for LatexSymbol {
+ fn into(self) -> DocumentSymbol {
+ let children = self.children.into_iter().map(Into::into).collect();
+ DocumentSymbol {
+ name: self.name,
+ deprecated: Some(self.deprecated),
+ detail: self.label,
+ kind: self.kind.into(),
+ selection_range: self.selection_range,
+ range: self.full_range,
+ children: Some(children),
+ }
+ }
+}
+
+pub struct SymbolProvider {
+ provider: ConcatProvider<DocumentSymbolParams, LatexSymbol>,
+}
+
+impl SymbolProvider {
+ pub fn new() -> Self {
+ Self {
+ provider: ConcatProvider::new(vec![
+ Box::new(BibtexEntrySymbolProvider),
+ Box::new(BibtexStringSymbolProvider),
+ Box::new(LatexSectionSymbolProvider),
+ ]),
+ }
+ }
+}
+
+impl Default for SymbolProvider {
+ fn default() -> Self {
+ Self::new()
+ }
+}
+
+impl FeatureProvider for SymbolProvider {
+ type Params = DocumentSymbolParams;
+ type Output = Vec<LatexSymbol>;
+
+ #[boxed]
+ async fn execute<'a>(&'a self, request: &'a FeatureRequest<Self::Params>) -> Self::Output {
+ self.provider.execute(request).await
+ }
+}
+
+#[serde(untagged)]
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
+pub enum SymbolResponse {
+ Flat(Vec<SymbolInformation>),
+ Hierarchical(Vec<DocumentSymbol>),
+}
+
+impl SymbolResponse {
+ pub fn new(
+ client_capabilities: &ClientCapabilities,
+ workspace: &Workspace,
+ uri: &Uri,
+ symbols: Vec<LatexSymbol>,
+ ) -> Self {
+ if client_capabilities.has_hierarchical_document_symbol_support() {
+ Self::Hierarchical(symbols.into_iter().map(Into::into).collect())
+ } else {
+ let mut buffer = Vec::new();
+ for symbol in symbols {
+ symbol.flatten(&mut buffer);
+ }
+ let mut buffer = buffer
+ .into_iter()
+ .map(|symbol| symbol.into_symbol_info(uri.clone()))
+ .collect();
+ sort_symbols(workspace, &mut buffer);
+ Self::Flat(buffer)
+ }
+ }
+}
+
+struct WorkspaceSymbol {
+ info: SymbolInformation,
+ search_text: String,
+}
+
+pub async fn workspace_symbols(
+ distribution: Arc<Box<dyn tex::Distribution>>,
+ client_capabilities: Arc<ClientCapabilities>,
+ workspace: Arc<Workspace>,
+ params: &WorkspaceSymbolParams,
+) -> Vec<SymbolInformation> {
+ let provider = SymbolProvider::new();
+ let mut symbols = Vec::new();
+
+ for document in &workspace.documents {
+ let uri: Uri = document.uri.clone();
+ let request = FeatureRequest {
+ client_capabilities: Arc::clone(&client_capabilities),
+ view: DocumentView::new(Arc::clone(&workspace), Arc::clone(&document)),
+ params: DocumentSymbolParams {
+ text_document: TextDocumentIdentifier::new(uri.clone().into()),
+ },
+ distribution: Arc::clone(&distribution),
+ };
+
+ let mut buffer = Vec::new();
+ for symbol in provider.execute(&request).await {
+ symbol.flatten(&mut buffer);
+ }
+
+ for symbol in buffer {
+ symbols.push(WorkspaceSymbol {
+ search_text: symbol.search_text(),
+ info: symbol.into_symbol_info(uri.clone()),
+ });
+ }
+ }
+
+ let query_words: Vec<_> = params
+ .query
+ .split_whitespace()
+ .map(str::to_lowercase)
+ .collect();
+ let mut filtered = Vec::new();
+ for symbol in symbols {
+ let mut included = true;
+ for word in &query_words {
+ if !symbol.search_text.contains(word) {
+ included = false;
+ break;
+ }
+ }
+
+ if included {
+ filtered.push(symbol.info);
+ }
+ }
+ sort_symbols(&workspace, &mut filtered);
+ filtered
+}
+
+fn sort_symbols(workspace: &Workspace, symbols: &mut Vec<SymbolInformation>) {
+ let ordering = ProjectOrdering::new(workspace);
+ symbols.sort_by(|left, right| {
+ let left_key = (
+ ordering.get(&Uri::from(left.location.uri.clone())),
+ left.location.range.start,
+ Reverse(left.location.range.end),
+ );
+ let right_key = (
+ ordering.get(&Uri::from(right.location.uri.clone())),
+ right.location.range.start,
+ Reverse(right.location.range.end),
+ );
+ left_key.cmp(&right_key)
+ });
+}
diff --git a/support/texlab/src/symbol/project_order.rs b/support/texlab/src/symbol/project_order.rs
new file mode 100644
index 0000000000..31b71f36bb
--- /dev/null
+++ b/support/texlab/src/symbol/project_order.rs
@@ -0,0 +1,142 @@
+use crate::syntax::*;
+use crate::workspace::*;
+use petgraph::algo::tarjan_scc;
+use petgraph::{Directed, Graph};
+use std::collections::HashSet;
+use std::sync::Arc;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct ProjectOrdering {
+ ordering: Vec<Arc<Document>>,
+}
+
+impl ProjectOrdering {
+ pub fn new(workspace: &Workspace) -> Self {
+ let mut ordering = Vec::new();
+ let connected_components = Self::connected_components(workspace);
+ for connected_component in connected_components {
+ let graph = Self::build_dependency_graph(&connected_component);
+
+ let mut visited = HashSet::new();
+ let root_index = *graph.node_weight(tarjan_scc(&graph)[0][0]).unwrap();
+ let mut stack = vec![Arc::clone(&connected_component[root_index])];
+
+ while let Some(document) = stack.pop() {
+ if !visited.insert(document.uri.as_str().to_owned()) {
+ continue;
+ }
+
+ ordering.push(Arc::clone(&document));
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ for include in tree.includes.iter().rev() {
+ for targets in &include.all_targets {
+ for target in targets {
+ if let Some(child) = workspace.find(target) {
+ stack.push(child);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Self { ordering }
+ }
+
+ fn connected_components(workspace: &Workspace) -> Vec<Vec<Arc<Document>>> {
+ let mut components = Vec::new();
+ let mut visited = HashSet::new();
+ for root in &workspace.documents {
+ if !visited.insert(root.uri.clone()) {
+ continue;
+ }
+
+ let component = workspace.related_documents(&root.uri);
+ for document in &component {
+ visited.insert(document.uri.clone());
+ }
+ components.push(component);
+ }
+ components
+ }
+
+ fn build_dependency_graph(documents: &[Arc<Document>]) -> Graph<usize, (), Directed> {
+ let mut graph = Graph::new();
+ let nodes: Vec<_> = (0..documents.len()).map(|i| graph.add_node(i)).collect();
+
+ for (i, document) in documents.iter().enumerate() {
+ if let SyntaxTree::Latex(tree) = &document.tree {
+ for targets in tree
+ .includes
+ .iter()
+ .flat_map(|include| &include.all_targets)
+ {
+ for target in targets {
+ if let Some(j) = documents.iter().position(|doc| doc.uri == *target) {
+ graph.add_edge(nodes[j], nodes[i], ());
+ break;
+ }
+ }
+ }
+ }
+ }
+ graph
+ }
+
+ pub fn get(&self, uri: &Uri) -> usize {
+ self.ordering
+ .iter()
+ .position(|doc| doc.uri == *uri)
+ .unwrap_or(std::usize::MAX)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_no_cycles() {
+ let mut builder = WorkspaceBuilder::new();
+ let a = builder.document("a.tex", "");
+ let b = builder.document("b.tex", "");
+ let c = builder.document("c.tex", "\\include{b}\\include{a}");
+
+ let project_ordering = ProjectOrdering::new(&builder.workspace);
+
+ assert_eq!(project_ordering.get(&a), 2);
+ assert_eq!(project_ordering.get(&b), 1);
+ assert_eq!(project_ordering.get(&c), 0);
+ }
+
+ #[test]
+ fn test_cycles() {
+ let mut builder = WorkspaceBuilder::new();
+ let a = builder.document("a.tex", "\\include{b}");
+ let b = builder.document("b.tex", "\\include{a}");
+ let c = builder.document("c.tex", "\\include{a}");
+
+ let project_ordering = ProjectOrdering::new(&builder.workspace);
+
+ assert_eq!(project_ordering.get(&a), 1);
+ assert_eq!(project_ordering.get(&b), 2);
+ assert_eq!(project_ordering.get(&c), 0);
+ }
+
+ #[test]
+ fn test_multiple_roots() {
+ let mut builder = WorkspaceBuilder::new();
+ let a = builder.document("a.tex", "\\include{b}");
+ let b = builder.document("b.tex", "");
+ let c = builder.document("c.tex", "");
+ let d = builder.document("d.tex", "\\include{c}");
+
+ let project_ordering = ProjectOrdering::new(&builder.workspace);
+
+ assert_eq!(project_ordering.get(&a), 0);
+ assert_eq!(project_ordering.get(&b), 1);
+ assert_eq!(project_ordering.get(&d), 2);
+ assert_eq!(project_ordering.get(&c), 3);
+ }
+}