diff options
Diffstat (limited to 'support/texlab/src/symbol')
-rw-r--r-- | support/texlab/src/symbol/bibtex_entry.rs | 147 | ||||
-rw-r--r-- | support/texlab/src/symbol/bibtex_string.rs | 79 | ||||
-rw-r--r-- | support/texlab/src/symbol/latex_section/enumeration.rs | 85 | ||||
-rw-r--r-- | support/texlab/src/symbol/latex_section/equation.rs | 41 | ||||
-rw-r--r-- | support/texlab/src/symbol/latex_section/float.rs | 57 | ||||
-rw-r--r-- | support/texlab/src/symbol/latex_section/mod.rs | 634 | ||||
-rw-r--r-- | support/texlab/src/symbol/latex_section/theorem.rs | 66 | ||||
-rw-r--r-- | support/texlab/src/symbol/mod.rs | 261 | ||||
-rw-r--r-- | support/texlab/src/symbol/project_order.rs | 142 |
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); + } +} |