summaryrefslogtreecommitdiff
path: root/support/texlab/crates/symbols/src
diff options
context:
space:
mode:
authorNorbert Preining <norbert@preining.info>2023-05-07 03:01:42 +0000
committerNorbert Preining <norbert@preining.info>2023-05-07 03:01:42 +0000
commite2a7938bcd22a142fa92c5c5d91f038f7ae73231 (patch)
tree93e20c35b061ab13d971c4333d535243a076eece /support/texlab/crates/symbols/src
parent507fa9c669d7e4bc2c808f269113c6ced0b18827 (diff)
CTAN sync 202305070301
Diffstat (limited to 'support/texlab/crates/symbols/src')
-rw-r--r--support/texlab/crates/symbols/src/document.rs51
-rw-r--r--support/texlab/crates/symbols/src/document/bib.rs64
-rw-r--r--support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__allowed_patterns.snap31
-rw-r--r--support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__enumerate.snap62
-rw-r--r--support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__equation.snap40
-rw-r--r--support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__float.snap40
-rw-r--r--support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__ignored_patterns.snap19
-rw-r--r--support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__section.snap41
-rw-r--r--support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__theorem.snap53
-rw-r--r--support/texlab/crates/symbols/src/document/tests.rs250
-rw-r--r--support/texlab/crates/symbols/src/document/tex.rs257
-rw-r--r--support/texlab/crates/symbols/src/lib.rs9
-rw-r--r--support/texlab/crates/symbols/src/types.rs89
-rw-r--r--support/texlab/crates/symbols/src/workspace.rs47
-rw-r--r--support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_bibtex.snap36
-rw-r--r--support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_figure.snap24
-rw-r--r--support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_item.snap60
-rw-r--r--support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_math.snap42
-rw-r--r--support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_section.snap78
-rw-r--r--support/texlab/crates/symbols/src/workspace/sort.rs203
-rw-r--r--support/texlab/crates/symbols/src/workspace/tests.rs99
21 files changed, 1595 insertions, 0 deletions
diff --git a/support/texlab/crates/symbols/src/document.rs b/support/texlab/crates/symbols/src/document.rs
new file mode 100644
index 0000000000..eb3d8d339f
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document.rs
@@ -0,0 +1,51 @@
+mod bib;
+mod tex;
+
+use base_db::{util, Document, DocumentData, SymbolConfig, Workspace};
+
+use crate::Symbol;
+
+pub fn document_symbols(workspace: &Workspace, document: &Document) -> Vec<Symbol> {
+ let project = workspace.project(document);
+ let mut symbols = match &document.data {
+ DocumentData::Tex(data) => {
+ let builder = tex::SymbolBuilder::new(&project, workspace.config());
+ builder.visit(&data.root_node())
+ }
+ DocumentData::Bib(data) => {
+ let builder = bib::SymbolBuilder;
+ data.root_node()
+ .children()
+ .filter_map(|node| builder.visit(&node))
+ .collect()
+ }
+ DocumentData::Aux(_)
+ | DocumentData::Log(_)
+ | DocumentData::Root
+ | DocumentData::Tectonic => Vec::new(),
+ };
+
+ filter_symbols(&mut symbols, &workspace.config().symbols);
+ symbols
+}
+
+fn filter_symbols(container: &mut Vec<Symbol>, config: &SymbolConfig) {
+ let allowed = &config.allowed_patterns;
+ let ignored = &config.ignored_patterns;
+
+ let mut i = 0;
+ while i < container.len() {
+ let symbol = &mut container[i];
+ if symbol.name.is_empty() || !util::filter_regex_patterns(&symbol.name, allowed, ignored) {
+ drop(symbol);
+ let mut symbol = container.remove(i);
+ container.append(&mut symbol.children);
+ } else {
+ filter_symbols(&mut symbol.children, config);
+ i += 1;
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/support/texlab/crates/symbols/src/document/bib.rs b/support/texlab/crates/symbols/src/document/bib.rs
new file mode 100644
index 0000000000..1e5d934a0d
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/bib.rs
@@ -0,0 +1,64 @@
+use base_db::data::{BibtexEntryType, BibtexEntryTypeCategory};
+use rowan::ast::AstNode;
+use syntax::bibtex::{self, HasName, HasType};
+
+use crate::{Symbol, SymbolKind};
+
+#[derive(Debug)]
+pub struct SymbolBuilder;
+
+impl SymbolBuilder {
+ pub fn visit(&self, node: &bibtex::SyntaxNode) -> Option<Symbol> {
+ if let Some(string) = bibtex::StringDef::cast(node.clone()) {
+ self.visit_string(&string)
+ } else if let Some(entry) = bibtex::Entry::cast(node.clone()) {
+ self.visit_entry(&entry)
+ } else {
+ None
+ }
+ }
+
+ fn visit_string(&self, string: &bibtex::StringDef) -> Option<Symbol> {
+ let name = string.name_token()?;
+ Some(Symbol {
+ name: name.text().into(),
+ kind: SymbolKind::Entry(BibtexEntryTypeCategory::String),
+ label: None,
+ full_range: string.syntax().text_range(),
+ selection_range: name.text_range(),
+ children: Vec::new(),
+ })
+ }
+
+ fn visit_entry(&self, entry: &bibtex::Entry) -> Option<Symbol> {
+ let ty = entry.type_token()?;
+ let key = entry.name_token()?;
+
+ let children = entry
+ .fields()
+ .filter_map(|field| self.visit_field(&field))
+ .collect();
+
+ let category = BibtexEntryType::find(&ty.text()[1..])
+ .map_or(BibtexEntryTypeCategory::Misc, |ty| ty.category);
+
+ Some(Symbol {
+ name: key.text().into(),
+ kind: SymbolKind::Entry(category),
+ label: None,
+ full_range: entry.syntax().text_range(),
+ selection_range: key.text_range(),
+ children,
+ })
+ }
+
+ fn visit_field(&self, field: &bibtex::Field) -> Option<Symbol> {
+ let name = field.name_token()?;
+ Some(Symbol::new_simple(
+ name.text().into(),
+ SymbolKind::Field,
+ field.syntax().text_range(),
+ name.text_range(),
+ ))
+ }
+}
diff --git a/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__allowed_patterns.snap b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__allowed_patterns.snap
new file mode 100644
index 0000000000..e22ce2a01d
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__allowed_patterns.snap
@@ -0,0 +1,31 @@
+---
+source: crates/symbols/src/document/tests.rs
+expression: "document_symbols(&fixture.workspace, document)"
+---
+[
+ Symbol {
+ name: "Enumerate",
+ kind: Enumeration,
+ label: None,
+ full_range: 98..159,
+ selection_range: 98..159,
+ children: [
+ Symbol {
+ name: "Item",
+ kind: EnumerationItem,
+ label: None,
+ full_range: 120..129,
+ selection_range: 120..129,
+ children: [],
+ },
+ Symbol {
+ name: "Item",
+ kind: EnumerationItem,
+ label: None,
+ full_range: 134..143,
+ selection_range: 134..143,
+ children: [],
+ },
+ ],
+ },
+]
diff --git a/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__enumerate.snap b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__enumerate.snap
new file mode 100644
index 0000000000..f94828c362
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__enumerate.snap
@@ -0,0 +1,62 @@
+---
+source: crates/symbols/src/document/tests.rs
+expression: "document_symbols(&fixture.workspace, document)"
+---
+[
+ Symbol {
+ name: "Enumerate",
+ kind: Enumeration,
+ label: None,
+ full_range: 43..184,
+ selection_range: 43..184,
+ children: [
+ Symbol {
+ name: "1",
+ kind: EnumerationItem,
+ label: Some(
+ Span(
+ "it:foo",
+ 70..84,
+ ),
+ ),
+ full_range: 65..88,
+ selection_range: 70..84,
+ children: [],
+ },
+ Symbol {
+ name: "Item",
+ kind: EnumerationItem,
+ label: Some(
+ Span(
+ "it:bar",
+ 98..112,
+ ),
+ ),
+ full_range: 93..116,
+ selection_range: 98..112,
+ children: [],
+ },
+ Symbol {
+ name: "Baz",
+ kind: EnumerationItem,
+ label: None,
+ full_range: 121..135,
+ selection_range: 121..135,
+ children: [],
+ },
+ Symbol {
+ name: "2",
+ kind: EnumerationItem,
+ label: Some(
+ Span(
+ "it:qux",
+ 150..164,
+ ),
+ ),
+ full_range: 140..168,
+ selection_range: 150..164,
+ children: [],
+ },
+ ],
+ },
+]
diff --git a/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__equation.snap b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__equation.snap
new file mode 100644
index 0000000000..0e284c1d71
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__equation.snap
@@ -0,0 +1,40 @@
+---
+source: crates/symbols/src/document/tests.rs
+expression: "document_symbols(&fixture.workspace, document)"
+---
+[
+ Symbol {
+ name: "Equation (1)",
+ kind: Equation,
+ label: Some(
+ Span(
+ "eq:foo",
+ 59..73,
+ ),
+ ),
+ full_range: 43..96,
+ selection_range: 59..73,
+ children: [],
+ },
+ Symbol {
+ name: "Equation",
+ kind: Equation,
+ label: Some(
+ Span(
+ "eq:bar",
+ 114..128,
+ ),
+ ),
+ full_range: 98..151,
+ selection_range: 114..128,
+ children: [],
+ },
+ Symbol {
+ name: "Equation",
+ kind: Equation,
+ label: None,
+ full_range: 153..192,
+ selection_range: 153..192,
+ children: [],
+ },
+]
diff --git a/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__float.snap b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__float.snap
new file mode 100644
index 0000000000..e9bd711c40
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__float.snap
@@ -0,0 +1,40 @@
+---
+source: crates/symbols/src/document/tests.rs
+expression: "document_symbols(&fixture.workspace, document)"
+---
+[
+ Symbol {
+ name: "Figure 1: Foo",
+ kind: Figure,
+ label: Some(
+ Span(
+ "fig:foo",
+ 83..98,
+ ),
+ ),
+ full_range: 43..111,
+ selection_range: 83..98,
+ children: [],
+ },
+ Symbol {
+ name: "Figure: Bar",
+ kind: Figure,
+ label: Some(
+ Span(
+ "fig:bar",
+ 153..168,
+ ),
+ ),
+ full_range: 113..181,
+ selection_range: 153..168,
+ children: [],
+ },
+ Symbol {
+ name: "Figure: Baz",
+ kind: Figure,
+ label: None,
+ full_range: 183..236,
+ selection_range: 183..236,
+ children: [],
+ },
+]
diff --git a/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__ignored_patterns.snap b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__ignored_patterns.snap
new file mode 100644
index 0000000000..044e2ca531
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__ignored_patterns.snap
@@ -0,0 +1,19 @@
+---
+source: crates/symbols/src/document/tests.rs
+expression: "document_symbols(&fixture.workspace, document)"
+---
+[
+ Symbol {
+ name: "Equation",
+ kind: Equation,
+ label: Some(
+ Span(
+ "eq:foo",
+ 59..73,
+ ),
+ ),
+ full_range: 43..96,
+ selection_range: 59..73,
+ children: [],
+ },
+]
diff --git a/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__section.snap b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__section.snap
new file mode 100644
index 0000000000..6cd44939cf
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__section.snap
@@ -0,0 +1,41 @@
+---
+source: crates/symbols/src/document/tests.rs
+expression: "document_symbols(&fixture.workspace, document)"
+---
+[
+ Symbol {
+ name: "Foo",
+ kind: Section,
+ label: None,
+ full_range: 43..56,
+ selection_range: 43..56,
+ children: [],
+ },
+ Symbol {
+ name: "2 Bar",
+ kind: Section,
+ label: Some(
+ Span(
+ "sec:bar",
+ 71..86,
+ ),
+ ),
+ full_range: 58..119,
+ selection_range: 71..86,
+ children: [
+ Symbol {
+ name: "Baz",
+ kind: Section,
+ label: Some(
+ Span(
+ "sec:baz",
+ 104..119,
+ ),
+ ),
+ full_range: 88..119,
+ selection_range: 104..119,
+ children: [],
+ },
+ ],
+ },
+]
diff --git a/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__theorem.snap b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__theorem.snap
new file mode 100644
index 0000000000..9d897532b1
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/snapshots/symbols__document__tests__theorem.snap
@@ -0,0 +1,53 @@
+---
+source: crates/symbols/src/document/tests.rs
+expression: "document_symbols(&fixture.workspace, document)"
+---
+[
+ Symbol {
+ name: "Lemma 1 (Foo)",
+ kind: Theorem,
+ label: Some(
+ Span(
+ "thm:foo",
+ 107..122,
+ ),
+ ),
+ full_range: 89..142,
+ selection_range: 107..122,
+ children: [],
+ },
+ Symbol {
+ name: "Lemma 2",
+ kind: Theorem,
+ label: Some(
+ Span(
+ "thm:bar",
+ 157..172,
+ ),
+ ),
+ full_range: 144..192,
+ selection_range: 157..172,
+ children: [],
+ },
+ Symbol {
+ name: "Lemma",
+ kind: Theorem,
+ label: Some(
+ Span(
+ "thm:baz",
+ 207..222,
+ ),
+ ),
+ full_range: 194..242,
+ selection_range: 207..222,
+ children: [],
+ },
+ Symbol {
+ name: "Lemma (Qux)",
+ kind: Theorem,
+ label: None,
+ full_range: 244..282,
+ selection_range: 244..282,
+ children: [],
+ },
+]
diff --git a/support/texlab/crates/symbols/src/document/tests.rs b/support/texlab/crates/symbols/src/document/tests.rs
new file mode 100644
index 0000000000..dcb3c17a7c
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/tests.rs
@@ -0,0 +1,250 @@
+use base_db::{Config, SymbolConfig};
+use insta::assert_debug_snapshot;
+use regex::Regex;
+use test_utils::fixture::Fixture;
+
+use crate::document_symbols;
+
+#[test]
+fn test_enumerate() {
+ let fixture = Fixture::parse(
+ r#"
+%! main.tex
+\documentclass{article}
+
+\begin{document}
+
+\begin{enumerate}
+ \item\label{it:foo} Foo
+ \item\label{it:bar} Bar
+ \item[Baz] Baz
+ \item[Qux]\label{it:qux} Qux
+\end{enumerate}
+
+\end{document}
+
+%! main.aux
+\relax
+\newlabel{it:foo}{{1}{1}}
+\newlabel{it:qux}{{2}{1}}"#,
+ );
+
+ let document = fixture.workspace.lookup(&fixture.documents[0].uri).unwrap();
+ assert_debug_snapshot!(document_symbols(&fixture.workspace, document));
+}
+
+#[test]
+fn test_equation() {
+ let fixture = Fixture::parse(
+ r#"
+%! main.tex
+\documentclass{article}
+
+\begin{document}
+
+\begin{equation}\label{eq:foo}
+ Foo
+\end{equation}
+
+\begin{equation}\label{eq:bar}
+ Bar
+\end{equation}
+
+\begin{equation}
+ Baz
+\end{equation}
+
+\end{document}
+
+%! main.aux
+\relax
+\newlabel{eq:foo}{{1}{1}}"#,
+ );
+
+ let document = fixture.workspace.lookup(&fixture.documents[0].uri).unwrap();
+ assert_debug_snapshot!(document_symbols(&fixture.workspace, document));
+}
+
+#[test]
+fn test_float() {
+ let fixture = Fixture::parse(
+ r#"
+%! main.tex
+\documentclass{article}
+
+\begin{document}
+
+\begin{figure}
+ Foo
+ \caption{Foo}\label{fig:foo}
+\end{figure}
+
+\begin{figure}
+ Bar
+ \caption{Bar}\label{fig:bar}
+\end{figure}
+
+\begin{figure}
+ Baz
+ \caption{Baz}
+\end{figure}
+
+\begin{figure}
+ Qux
+\end{figure}
+
+\end{document}
+|
+
+%! main.aux
+\relax
+\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Foo}}{1}\protected@file@percent }
+\newlabel{fig:foo}{{1}{1}}
+\@writefile{lof}{\contentsline {figure}{\numberline {2}{\ignorespaces Bar}}{1}\protected@file@percent }
+\@writefile{lof}{\contentsline {figure}{\numberline {3}{\ignorespaces Baz}}{1}\protected@file@percent }"#,
+ );
+
+ let document = fixture.workspace.lookup(&fixture.documents[0].uri).unwrap();
+ assert_debug_snapshot!(document_symbols(&fixture.workspace, document));
+}
+
+#[test]
+fn test_section() {
+ let fixture = Fixture::parse(
+ r#"
+%! main.tex
+\documentclass{article}
+
+\begin{document}
+
+\section{Foo}
+
+\section{Bar}\label{sec:bar}
+
+\subsection{Baz}\label{sec:baz}
+
+\end{document}
+|
+
+%! main.aux
+\relax
+\@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
+\@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{1}\protected@file@percent }
+\newlabel{sec:bar}{{2}{1}}"#,
+ );
+
+ let document = fixture.workspace.lookup(&fixture.documents[0].uri).unwrap();
+ assert_debug_snapshot!(document_symbols(&fixture.workspace, document));
+}
+
+#[test]
+fn test_theorem() {
+ let fixture = Fixture::parse(
+ r#"
+%! main.tex
+\documentclass{article}
+\usepackage{amsthm}
+\newtheorem{lemma}{Lemma}
+
+\begin{document}
+
+\begin{lemma}[Foo]\label{thm:foo}
+ Foo
+\end{lemma}
+
+\begin{lemma}\label{thm:bar}
+ Bar
+\end{lemma}
+
+\begin{lemma}\label{thm:baz}
+ Baz
+\end{lemma}
+
+\begin{lemma}[Qux]
+ Qux
+\end{lemma}
+
+\end{document}
+|
+
+%! main.aux
+\relax
+\newlabel{thm:foo}{{1}{1}}
+\newlabel{thm:bar}{{2}{1}}"#,
+ );
+
+ let document = fixture.workspace.lookup(&fixture.documents[0].uri).unwrap();
+ assert_debug_snapshot!(document_symbols(&fixture.workspace, document));
+}
+
+#[test]
+fn test_allowed_patterns() {
+ let mut fixture = Fixture::parse(
+ r#"
+%! main.tex
+\documentclass{article}
+
+\begin{document}
+
+\begin{equation}\label{eq:foo}
+ Foo
+\end{equation}
+
+\begin{enumerate}
+ \item Foo
+ \item Bar
+\end{enumerate}
+
+\end{document}"#,
+ );
+
+ fixture.workspace.set_config(Config {
+ symbols: SymbolConfig {
+ allowed_patterns: vec![
+ Regex::new("Item").unwrap(),
+ Regex::new("Enumerate").unwrap(),
+ ],
+ ignored_patterns: Vec::new(),
+ },
+ ..Config::default()
+ });
+
+ let document = fixture.workspace.lookup(&fixture.documents[0].uri).unwrap();
+ assert_debug_snapshot!(document_symbols(&fixture.workspace, document));
+}
+
+#[test]
+fn test_ignored_patterns() {
+ let mut fixture = Fixture::parse(
+ r#"
+%! main.tex
+\documentclass{article}
+
+\begin{document}
+
+\begin{equation}\label{eq:foo}
+ Foo
+\end{equation}
+
+\begin{enumerate}
+ \item Foo
+ \item Bar
+\end{enumerate}
+
+\end{document}"#,
+ );
+
+ fixture.workspace.set_config(Config {
+ symbols: SymbolConfig {
+ ignored_patterns: vec![
+ Regex::new("Item").unwrap(),
+ Regex::new("Enumerate").unwrap(),
+ ],
+ allowed_patterns: Vec::new(),
+ },
+ ..Config::default()
+ });
+
+ let document = fixture.workspace.lookup(&fixture.documents[0].uri).unwrap();
+ assert_debug_snapshot!(document_symbols(&fixture.workspace, document));
+}
diff --git a/support/texlab/crates/symbols/src/document/tex.rs b/support/texlab/crates/symbols/src/document/tex.rs
new file mode 100644
index 0000000000..55612ee11d
--- /dev/null
+++ b/support/texlab/crates/symbols/src/document/tex.rs
@@ -0,0 +1,257 @@
+use std::str::FromStr;
+
+use base_db::{semantics::Span, util::FloatKind, Config, Project};
+use rowan::ast::AstNode;
+use syntax::latex::{self, HasBrack, HasCurly, LatexLanguage};
+use titlecase::titlecase;
+
+use crate::{Symbol, SymbolKind};
+
+#[derive(Debug)]
+pub struct SymbolBuilder<'a> {
+ project: &'a Project<'a>,
+ config: &'a Config,
+}
+
+impl<'a> SymbolBuilder<'a> {
+ pub fn new(project: &'a Project<'a>, config: &'a Config) -> Self {
+ Self { project, config }
+ }
+
+ pub fn visit(&self, node: &latex::SyntaxNode) -> Vec<Symbol> {
+ let symbol = if let Some(section) = latex::Section::cast(node.clone()) {
+ self.visit_section(&section)
+ } else if let Some(enum_item) = latex::EnumItem::cast(node.clone()) {
+ self.visit_enum_item(&enum_item)
+ } else if let Some(equation) = latex::Equation::cast(node.clone()) {
+ self.visit_equation(&equation)
+ } else if let Some(environment) = latex::Environment::cast(node.clone()) {
+ environment.begin().and_then(|begin| {
+ let name = begin.name()?.key()?.to_string();
+ if self.config.syntax.math_environments.contains(&name) {
+ self.visit_equation(&environment)
+ } else if self.config.syntax.enum_environments.contains(&name) {
+ self.visit_enumeration(&environment, &name)
+ } else if let Ok(float_kind) = FloatKind::from_str(&name) {
+ self.visit_float(&environment, float_kind)
+ } else {
+ self.visit_theorem(&environment, &name)
+ }
+ })
+ } else {
+ None
+ };
+
+ match symbol {
+ Some(mut parent) => {
+ for child in node.children() {
+ parent.children.append(&mut self.visit(&child));
+ }
+
+ vec![parent]
+ }
+ None => {
+ let mut symbols = Vec::new();
+ for child in node.children() {
+ symbols.append(&mut self.visit(&child));
+ }
+
+ symbols
+ }
+ }
+ }
+
+ fn visit_section(&self, section: &latex::Section) -> Option<Symbol> {
+ let range = latex::small_range(section);
+ let group = section.name()?;
+ let group_text = group.content_text()?;
+ let kind = SymbolKind::Section;
+
+ let symbol = match self.find_label(section.syntax()) {
+ Some(label) => {
+ let name = match self.find_label_number(&label.text) {
+ Some(number) => format!("{number} {group_text}"),
+ None => group_text,
+ };
+
+ Symbol::new_label(name, kind, range, label)
+ }
+ None => Symbol::new_simple(group_text, kind, range, range),
+ };
+
+ Some(symbol)
+ }
+
+ fn visit_enum_item(&self, enum_item: &latex::EnumItem) -> Option<Symbol> {
+ let enum_envs = &self.config.syntax.enum_environments;
+ if !enum_item
+ .syntax()
+ .ancestors()
+ .filter_map(latex::Environment::cast)
+ .filter_map(|environment| environment.begin())
+ .filter_map(|begin| begin.name())
+ .filter_map(|name| name.key())
+ .any(|name| enum_envs.contains(&name.to_string()))
+ {
+ return None;
+ }
+
+ let range = latex::small_range(enum_item);
+ let kind = SymbolKind::EnumerationItem;
+ let name = enum_item
+ .label()
+ .and_then(|label| label.content_text())
+ .unwrap_or_else(|| "Item".into());
+
+ let symbol = match self.find_label(enum_item.syntax()) {
+ Some(label) => {
+ let name = self
+ .find_label_number(&label.text)
+ .map_or_else(|| name.clone(), String::from);
+
+ Symbol::new_label(name, kind, range, label)
+ }
+ None => Symbol::new_simple(name, kind, range, range),
+ };
+
+ Some(symbol)
+ }
+
+ fn visit_theorem(&self, environment: &latex::Environment, name: &str) -> Option<Symbol> {
+ let heading = self
+ .project
+ .documents
+ .iter()
+ .filter_map(|document| document.data.as_tex())
+ .flat_map(|data| data.semantics.theorem_definitions.iter())
+ .find(|theorem| theorem.name.text == name)
+ .map(|theorem| theorem.heading.as_str())?;
+
+ let options = environment.begin().and_then(|begin| begin.options());
+ let caption = options.and_then(|options| options.content_text());
+ let range = latex::small_range(environment);
+ let kind = SymbolKind::Theorem;
+
+ let symbol = match self.find_label(environment.syntax()) {
+ Some(label) => {
+ let name = match (self.find_label_number(&label.text), caption) {
+ (Some(number), Some(caption)) => {
+ format!("{heading} {number} ({caption})")
+ }
+ (Some(number), None) => format!("{heading} {number}"),
+ (None, Some(caption)) => format!("{heading} ({caption})"),
+ (None, None) => heading.into(),
+ };
+
+ Symbol::new_label(name, kind, range, label)
+ }
+ None => {
+ let name = caption.map_or_else(
+ || heading.into(),
+ |caption| format!("{heading} ({caption})"),
+ );
+
+ Symbol::new_simple(name, kind, range, range)
+ }
+ };
+
+ Some(symbol)
+ }
+
+ fn visit_float(
+ &self,
+ environment: &latex::Environment,
+ float_kind: FloatKind,
+ ) -> Option<Symbol> {
+ let range = latex::small_range(environment);
+
+ let (float_kind, symbol_kind) = match float_kind {
+ FloatKind::Algorithm => ("Algorithm", SymbolKind::Algorithm),
+ FloatKind::Figure => ("Figure", SymbolKind::Figure),
+ FloatKind::Listing => ("Listing", SymbolKind::Listing),
+ FloatKind::Table => ("Table", SymbolKind::Table),
+ };
+
+ let caption = environment
+ .syntax()
+ .children()
+ .filter_map(latex::Caption::cast)
+ .find_map(|node| node.long())
+ .and_then(|node| node.content_text())?;
+
+ let symbol = match self.find_label(environment.syntax()) {
+ Some(label) => {
+ let name = match self.find_label_number(&label.text) {
+ Some(number) => format!("{float_kind} {number}: {caption}"),
+ None => format!("{float_kind}: {caption}"),
+ };
+
+ Symbol::new_label(name, symbol_kind, range, label)
+ }
+ None => {
+ let name = format!("{float_kind}: {caption}");
+ Symbol::new_simple(name, symbol_kind, range, range)
+ }
+ };
+
+ Some(symbol)
+ }
+
+ fn visit_enumeration(
+ &self,
+ environment: &latex::Environment,
+ environment_name: &str,
+ ) -> Option<Symbol> {
+ let range = latex::small_range(environment);
+
+ let kind = SymbolKind::Enumeration;
+ let name = titlecase(environment_name);
+ let symbol = match self.find_label(environment.syntax()) {
+ Some(label) => {
+ let name = match self.find_label_number(&label.text) {
+ Some(number) => format!("{name} {number}"),
+ None => name,
+ };
+
+ Symbol::new_label(name, kind, range, label)
+ }
+ None => Symbol::new_simple(name, kind, range, range),
+ };
+
+ Some(symbol)
+ }
+
+ fn visit_equation(&self, node: &dyn AstNode<Language = LatexLanguage>) -> Option<Symbol> {
+ let range = latex::small_range(node);
+ let kind = SymbolKind::Equation;
+ let symbol = match self.find_label(node.syntax()) {
+ Some(label) => {
+ let name = match self.find_label_number(&label.text) {
+ Some(number) => format!("Equation ({number})"),
+ None => "Equation".into(),
+ };
+
+ Symbol::new_label(name, kind, range, label)
+ }
+ None => Symbol::new_simple("Equation".into(), kind, range, range),
+ };
+
+ Some(symbol)
+ }
+
+ fn find_label(&self, parent: &latex::SyntaxNode) -> Option<Span> {
+ let label = parent.children().find_map(latex::LabelDefinition::cast)?;
+ let range = latex::small_range(&label);
+ let text = label.name()?.key()?.to_string();
+ Some(Span { text, range })
+ }
+
+ fn find_label_number(&self, name: &str) -> Option<&str> {
+ self.project
+ .documents
+ .iter()
+ .filter_map(|document| document.data.as_aux())
+ .find_map(|data| data.semantics.label_numbers.get(name))
+ .map(String::as_str)
+ }
+}
diff --git a/support/texlab/crates/symbols/src/lib.rs b/support/texlab/crates/symbols/src/lib.rs
new file mode 100644
index 0000000000..99822a1993
--- /dev/null
+++ b/support/texlab/crates/symbols/src/lib.rs
@@ -0,0 +1,9 @@
+mod document;
+mod types;
+mod workspace;
+
+pub use self::{
+ document::document_symbols,
+ types::{Symbol, SymbolKind, SymbolLocation},
+ workspace::workspace_symbols,
+};
diff --git a/support/texlab/crates/symbols/src/types.rs b/support/texlab/crates/symbols/src/types.rs
new file mode 100644
index 0000000000..d78f9af200
--- /dev/null
+++ b/support/texlab/crates/symbols/src/types.rs
@@ -0,0 +1,89 @@
+use base_db::{data::BibtexEntryTypeCategory, semantics::Span, Document};
+use rowan::TextRange;
+
+#[derive(Debug, PartialEq, Eq, Clone, Copy)]
+pub enum SymbolKind {
+ Section,
+ Figure,
+ Algorithm,
+ Table,
+ Listing,
+ Enumeration,
+ EnumerationItem,
+ Theorem,
+ Equation,
+ Entry(BibtexEntryTypeCategory),
+ Field,
+}
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct Symbol {
+ pub name: String,
+ pub kind: SymbolKind,
+ pub label: Option<Span>,
+ pub full_range: TextRange,
+ pub selection_range: TextRange,
+ pub children: Vec<Symbol>,
+}
+
+impl Symbol {
+ pub fn new_simple(
+ name: String,
+ kind: SymbolKind,
+ full_range: TextRange,
+ selection_range: TextRange,
+ ) -> Self {
+ Self {
+ name,
+ kind,
+ label: None,
+ full_range,
+ selection_range,
+ children: Vec::new(),
+ }
+ }
+
+ pub fn new_label(name: String, kind: SymbolKind, range: TextRange, label: Span) -> Self {
+ Self {
+ name,
+ kind,
+ full_range: range,
+ selection_range: label.range,
+ label: Some(label),
+ children: Vec::new(),
+ }
+ }
+
+ pub fn keywords<'a>(&'a self) -> Vec<&'a str> {
+ match self.kind {
+ SymbolKind::Section => vec![&self.name, "latex", "section"],
+ SymbolKind::Figure => vec![&self.name, "latex", "float", "figure"],
+ SymbolKind::Algorithm => vec![&self.name, "latex", "float", "algorithm"],
+ SymbolKind::Table => vec![&self.name, "latex", "float", "table"],
+ SymbolKind::Listing => vec![&self.name, "latex", "float", "listing"],
+ SymbolKind::Enumeration => vec![&self.name, "latex", "enumeration"],
+ SymbolKind::EnumerationItem => vec![&self.name, "latex", "enumeration", "item"],
+ SymbolKind::Theorem => vec![&self.name, "latex", "math"],
+ SymbolKind::Equation => vec![&self.name, "latex", "math", "equation"],
+ SymbolKind::Entry(BibtexEntryTypeCategory::String) => {
+ vec![&self.name, "bibtex", "string"]
+ }
+ SymbolKind::Entry(_) => vec![&self.name, "bibtex", "entry"],
+ SymbolKind::Field => vec![&self.name, "bibtex", "field"],
+ }
+ }
+
+ pub fn flatten(mut self, buffer: &mut Vec<Self>) {
+ for symbol in self.children.drain(..) {
+ symbol.flatten(buffer);
+ }
+
+ buffer.push(self);
+ }
+}
+
+#[derive(Debug)]
+pub struct SymbolLocation<'a> {
+ pub document: &'a Document,
+ pub symbol: Symbol,
+}
diff --git a/support/texlab/crates/symbols/src/workspace.rs b/support/texlab/crates/symbols/src/workspace.rs
new file mode 100644
index 0000000000..ad9206b2c5
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace.rs
@@ -0,0 +1,47 @@
+mod sort;
+
+use std::cmp::Reverse;
+
+use base_db::Workspace;
+
+use crate::{document_symbols, types::SymbolLocation, SymbolKind};
+
+use self::sort::ProjectOrdering;
+
+pub fn workspace_symbols<'a>(workspace: &'a Workspace, query: &str) -> Vec<SymbolLocation<'a>> {
+ let query = query.split_whitespace().collect::<Vec<_>>();
+ let mut results = Vec::new();
+
+ for document in workspace.iter() {
+ let mut buf = Vec::new();
+ let symbols = document_symbols(workspace, document);
+ for symbol in symbols {
+ symbol.flatten(&mut buf);
+ }
+
+ for symbol in buf
+ .into_iter()
+ .filter(|symbol| symbol.kind != SymbolKind::Field)
+ {
+ let keywords = symbol.keywords();
+ if query.is_empty()
+ || itertools::iproduct!(keywords.iter(), query.iter())
+ .any(|(keyword, query)| keyword.eq_ignore_ascii_case(query))
+ {
+ results.push(SymbolLocation { document, symbol });
+ }
+ }
+ }
+
+ let ordering = ProjectOrdering::from(workspace);
+ results.sort_by_key(|item| {
+ let index = ordering.get(&item.document.uri);
+ let range = item.symbol.full_range;
+ (index, range.start(), Reverse(range.end()))
+ });
+
+ results
+}
+
+#[cfg(test)]
+mod tests;
diff --git a/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_bibtex.snap b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_bibtex.snap
new file mode 100644
index 0000000000..5bd00182b0
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_bibtex.snap
@@ -0,0 +1,36 @@
+---
+source: crates/symbols/src/workspace/tests.rs
+expression: "workspace_symbols(&fixture.workspace, \"bibtex\")"
+---
+[
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.bib",
+ ),
+ symbol: Symbol {
+ name: "foo",
+ kind: Entry(
+ Article,
+ ),
+ label: None,
+ full_range: 0..14,
+ selection_range: 9..12,
+ children: [],
+ },
+ },
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.bib",
+ ),
+ symbol: Symbol {
+ name: "bar",
+ kind: Entry(
+ String,
+ ),
+ label: None,
+ full_range: 16..36,
+ selection_range: 24..27,
+ children: [],
+ },
+ },
+]
diff --git a/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_figure.snap b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_figure.snap
new file mode 100644
index 0000000000..412e7c14d2
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_figure.snap
@@ -0,0 +1,24 @@
+---
+source: crates/symbols/src/workspace/tests.rs
+expression: "workspace_symbols(&fixture.workspace, \"figure\")"
+---
+[
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "Figure 1: Bar",
+ kind: Figure,
+ label: Some(
+ Span(
+ "fig:bar",
+ 265..280,
+ ),
+ ),
+ full_range: 220..293,
+ selection_range: 265..280,
+ children: [],
+ },
+ },
+]
diff --git a/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_item.snap b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_item.snap
new file mode 100644
index 0000000000..993d046d3d
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_item.snap
@@ -0,0 +1,60 @@
+---
+source: crates/symbols/src/workspace/tests.rs
+expression: "workspace_symbols(&fixture.workspace, \"item\")"
+---
+[
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "1",
+ kind: EnumerationItem,
+ label: Some(
+ Span(
+ "itm:foo",
+ 352..367,
+ ),
+ ),
+ full_range: 347..371,
+ selection_range: 352..367,
+ children: [],
+ },
+ },
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "2",
+ kind: EnumerationItem,
+ label: Some(
+ Span(
+ "itm:bar",
+ 381..396,
+ ),
+ ),
+ full_range: 376..400,
+ selection_range: 381..396,
+ children: [],
+ },
+ },
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "3",
+ kind: EnumerationItem,
+ label: Some(
+ Span(
+ "itm:baz",
+ 410..425,
+ ),
+ ),
+ full_range: 405..429,
+ selection_range: 410..425,
+ children: [],
+ },
+ },
+]
diff --git a/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_math.snap b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_math.snap
new file mode 100644
index 0000000000..60a002fba5
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_math.snap
@@ -0,0 +1,42 @@
+---
+source: crates/symbols/src/workspace/tests.rs
+expression: "workspace_symbols(&fixture.workspace, \"math\")"
+---
+[
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "Equation (1)",
+ kind: Equation,
+ label: Some(
+ Span(
+ "eq:foo",
+ 151..165,
+ ),
+ ),
+ full_range: 135..188,
+ selection_range: 151..165,
+ children: [],
+ },
+ },
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "Lemma 1 (Qux)",
+ kind: Theorem,
+ label: Some(
+ Span(
+ "thm:qux",
+ 522..537,
+ ),
+ ),
+ full_range: 504..557,
+ selection_range: 522..537,
+ children: [],
+ },
+ },
+]
diff --git a/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_section.snap b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_section.snap
new file mode 100644
index 0000000000..4cf0cb5fe2
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace/snapshots/symbols__workspace__tests__filter_type_section.snap
@@ -0,0 +1,78 @@
+---
+source: crates/symbols/src/workspace/tests.rs
+expression: "workspace_symbols(&fixture.workspace, \"section\")"
+---
+[
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "1 Foo",
+ kind: Section,
+ label: Some(
+ Span(
+ "sec:foo",
+ 118..133,
+ ),
+ ),
+ full_range: 105..188,
+ selection_range: 118..133,
+ children: [],
+ },
+ },
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "2 Bar",
+ kind: Section,
+ label: Some(
+ Span(
+ "sec:bar",
+ 203..218,
+ ),
+ ),
+ full_range: 190..293,
+ selection_range: 203..218,
+ children: [],
+ },
+ },
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "3 Baz",
+ kind: Section,
+ label: Some(
+ Span(
+ "sec:baz",
+ 308..323,
+ ),
+ ),
+ full_range: 295..445,
+ selection_range: 308..323,
+ children: [],
+ },
+ },
+ SymbolLocation {
+ document: Document(
+ "file:///texlab/main.tex",
+ ),
+ symbol: Symbol {
+ name: "4 Qux",
+ kind: Section,
+ label: Some(
+ Span(
+ "sec:qux",
+ 460..475,
+ ),
+ ),
+ full_range: 447..557,
+ selection_range: 460..475,
+ children: [],
+ },
+ },
+]
diff --git a/support/texlab/crates/symbols/src/workspace/sort.rs b/support/texlab/crates/symbols/src/workspace/sort.rs
new file mode 100644
index 0000000000..bc30728da9
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace/sort.rs
@@ -0,0 +1,203 @@
+use base_db::{graph, Document, Workspace};
+use itertools::Itertools;
+use url::Url;
+
+#[derive(Debug, PartialEq, Eq, Clone)]
+pub struct ProjectOrdering<'a> {
+ inner: Vec<&'a Document>,
+}
+
+impl<'a> ProjectOrdering<'a> {
+ pub fn get(&self, uri: &Url) -> usize {
+ self.inner
+ .iter()
+ .position(|doc| doc.uri == *uri)
+ .unwrap_or(std::usize::MAX)
+ }
+}
+
+impl<'a> From<&'a Workspace> for ProjectOrdering<'a> {
+ fn from(workspace: &'a Workspace) -> Self {
+ let inner = workspace
+ .iter()
+ .filter(|document| {
+ let data = document.data.as_tex();
+ data.map_or(false, |data| data.semantics.can_be_root)
+ })
+ .chain(workspace.iter())
+ .flat_map(|document| {
+ let graph = graph::Graph::new(workspace, document);
+ graph.preorder().rev().collect_vec()
+ })
+ .unique()
+ .collect_vec();
+
+ Self { inner }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use base_db::Owner;
+ use distro::Language;
+ use rowan::TextSize;
+
+ use super::{ProjectOrdering, Url, Workspace};
+
+ #[test]
+ fn test_no_cycles() {
+ let mut workspace = Workspace::default();
+
+ let a = Url::parse("http://example.com/a.tex").unwrap();
+ let b = Url::parse("http://example.com/b.tex").unwrap();
+ let c = Url::parse("http://example.com/c.tex").unwrap();
+
+ workspace.open(
+ a.clone(),
+ String::new(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ b.clone(),
+ String::new(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ c.clone(),
+ r#"\documentclass{article}\include{b}\include{a}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ let ordering = ProjectOrdering::from(&workspace);
+ assert_eq!(ordering.get(&a), 0);
+ assert_eq!(ordering.get(&b), 1);
+ assert_eq!(ordering.get(&c), 2);
+ }
+
+ #[test]
+ fn test_two_layers() {
+ let mut workspace = Workspace::default();
+
+ let a = Url::parse("http://example.com/a.tex").unwrap();
+ let b = Url::parse("http://example.com/b.tex").unwrap();
+ let c = Url::parse("http://example.com/c.tex").unwrap();
+
+ workspace.open(
+ a.clone(),
+ String::new(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ b.clone(),
+ r#"\include{a}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ c.clone(),
+ r#"\begin{documnent}\include{b}\end{document}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ let ordering = ProjectOrdering::from(&workspace);
+ assert_eq!(ordering.get(&a), 0);
+ assert_eq!(ordering.get(&b), 1);
+ assert_eq!(ordering.get(&c), 2);
+ }
+
+ #[test]
+ fn test_cycles() {
+ let mut workspace = Workspace::default();
+
+ let a = Url::parse("http://example.com/a.tex").unwrap();
+ let b = Url::parse("http://example.com/b.tex").unwrap();
+ let c = Url::parse("http://example.com/c.tex").unwrap();
+ workspace.open(
+ a.clone(),
+ r#"\begin{document}\include{b}\end{document}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ b.clone(),
+ r#"\include{a}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ c.clone(),
+ r#"\include{a}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ let ordering = ProjectOrdering::from(&workspace);
+ assert_ne!(ordering.get(&a), 0);
+ }
+
+ #[test]
+ fn test_multiple_roots() {
+ let mut workspace = Workspace::default();
+
+ let a = Url::parse("http://example.com/a.tex").unwrap();
+ let b = Url::parse("http://example.com/b.tex").unwrap();
+ let c = Url::parse("http://example.com/c.tex").unwrap();
+ let d = Url::parse("http://example.com/d.tex").unwrap();
+
+ workspace.open(
+ a.clone(),
+ r#"\begin{document}\include{b}\end{document}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ b.clone(),
+ String::new(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ c.clone(),
+ String::new(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ workspace.open(
+ d.clone(),
+ r#"\begin{document}\include{c}\end{document}"#.to_string(),
+ Language::Tex,
+ Owner::Client,
+ TextSize::default(),
+ );
+
+ let ordering = ProjectOrdering::from(&workspace);
+ assert!(ordering.get(&b) < ordering.get(&a));
+ assert!(ordering.get(&c) < ordering.get(&d));
+ }
+}
diff --git a/support/texlab/crates/symbols/src/workspace/tests.rs b/support/texlab/crates/symbols/src/workspace/tests.rs
new file mode 100644
index 0000000000..1231be869d
--- /dev/null
+++ b/support/texlab/crates/symbols/src/workspace/tests.rs
@@ -0,0 +1,99 @@
+use insta::assert_debug_snapshot;
+use test_utils::fixture::Fixture;
+
+use crate::workspace_symbols;
+
+static FIXTURE: &str = r#"
+%! main.tex
+\documentclass{article}
+\usepackage{caption}
+\usepackage{amsmath}
+\usepackage{amsthm}
+
+\begin{document}
+
+\section{Foo}\label{sec:foo}
+
+\begin{equation}\label{eq:foo}
+ Foo
+\end{equation}
+
+\section{Bar}\label{sec:bar}
+
+\begin{figure}
+ Bar
+ \caption{Bar}
+ \label{fig:bar}
+\end{figure}
+
+\section{Baz}\label{sec:baz}
+
+\begin{enumerate}
+ \item\label{itm:foo} Foo
+ \item\label{itm:bar} Bar
+ \item\label{itm:baz} Baz
+\end{enumerate}
+
+\section{Qux}\label{sec:qux}
+
+\newtheorem{lemma}{Lemma}
+
+\begin{lemma}[Qux]\label{thm:qux}
+ Qux
+\end{lemma}
+
+\end{document}
+
+%! main.aux
+\relax
+\@writefile{lof}{\contentsline {figure}{\numberline {1}{\ignorespaces Bar\relax }}{1}\protected@file@percent }
+\providecommand*\caption@xref[2]{\@setref\relax\@undefined{#1}}
+\newlabel{fig:bar}{{1}{1}}
+\@writefile{toc}{\contentsline {section}{\numberline {1}Foo}{1}\protected@file@percent }
+\newlabel{sec:foo}{{1}{1}}
+\newlabel{eq:foo}{{1}{1}}
+\@writefile{toc}{\contentsline {section}{\numberline {2}Bar}{1}\protected@file@percent }
+\newlabel{sec:bar}{{2}{1}}
+\@writefile{toc}{\contentsline {section}{\numberline {3}Baz}{1}\protected@file@percent }
+\newlabel{sec:baz}{{3}{1}}
+\newlabel{itm:foo}{{1}{1}}
+\newlabel{itm:bar}{{2}{1}}
+\newlabel{itm:baz}{{3}{1}}
+\@writefile{toc}{\contentsline {section}{\numberline {4}Qux}{1}\protected@file@percent }
+\newlabel{sec:qux}{{4}{1}}
+\newlabel{thm:qux}{{1}{1}}
+
+%! main.bib
+@article{foo,}
+
+@string{bar = "bar"}"#;
+
+#[test]
+fn filter_type_section() {
+ let fixture = Fixture::parse(FIXTURE);
+ assert_debug_snapshot!(workspace_symbols(&fixture.workspace, "section"));
+}
+
+#[test]
+fn filter_type_figure() {
+ let fixture = Fixture::parse(FIXTURE);
+ assert_debug_snapshot!(workspace_symbols(&fixture.workspace, "figure"));
+}
+
+#[test]
+fn filter_type_item() {
+ let fixture = Fixture::parse(FIXTURE);
+ assert_debug_snapshot!(workspace_symbols(&fixture.workspace, "item"));
+}
+
+#[test]
+fn filter_type_math() {
+ let fixture = Fixture::parse(FIXTURE);
+ assert_debug_snapshot!(workspace_symbols(&fixture.workspace, "math"));
+}
+
+#[test]
+fn filter_bibtex() {
+ let fixture = Fixture::parse(FIXTURE);
+ assert_debug_snapshot!(workspace_symbols(&fixture.workspace, "bibtex"));
+}