diff options
author | Norbert Preining <norbert@preining.info> | 2023-05-07 03:01:42 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2023-05-07 03:01:42 +0000 |
commit | e2a7938bcd22a142fa92c5c5d91f038f7ae73231 (patch) | |
tree | 93e20c35b061ab13d971c4333d535243a076eece /support/texlab/crates/symbols/src | |
parent | 507fa9c669d7e4bc2c808f269113c6ced0b18827 (diff) |
CTAN sync 202305070301
Diffstat (limited to 'support/texlab/crates/symbols/src')
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(§ion) + } 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")); +} |