diff options
Diffstat (limited to 'support/texlab/src/features/symbol/latex.rs')
-rw-r--r-- | support/texlab/src/features/symbol/latex.rs | 488 |
1 files changed, 488 insertions, 0 deletions
diff --git a/support/texlab/src/features/symbol/latex.rs b/support/texlab/src/features/symbol/latex.rs new file mode 100644 index 0000000000..ce584c0c76 --- /dev/null +++ b/support/texlab/src/features/symbol/latex.rs @@ -0,0 +1,488 @@ +use std::str::FromStr; + +use cancellation::CancellationToken; +use lsp_types::Range; +use smol_str::SmolStr; +use titlecase::titlecase; + +use crate::{ + find_caption_by_parent, find_label_number, + syntax::{ + latex::{self, HasBrack, HasCurly}, + CstNode, + }, + LabelledFloatKind, LatexDocumentData, LineIndexExt, WorkspaceSubset, LANGUAGE_DATA, +}; + +use super::types::{InternalSymbol, InternalSymbolKind}; + +pub fn find_latex_symbols( + subset: &WorkspaceSubset, + buf: &mut Vec<InternalSymbol>, + token: &CancellationToken, +) -> Option<()> { + let main_document = subset + .documents + .first() + .filter(|document| document.uri.as_str().ends_with(".tex"))?; + + let data = main_document.data.as_latex()?; + let mut context = Context { + subset, + data, + token, + }; + + let root = &context.data.root; + let mut symbols = visit(&mut context, root); + buf.append(&mut symbols); + Some(()) +} + +struct Context<'a> { + subset: &'a WorkspaceSubset, + data: &'a LatexDocumentData, + token: &'a CancellationToken, +} + +fn visit(context: &mut Context, node: &latex::SyntaxNode) -> Vec<InternalSymbol> { + let symbol = match node.kind() { + latex::PART + | latex::CHAPTER + | latex::SECTION + | latex::SUBSECTION + | latex::SUBSUBSECTION + | latex::PARAGRAPH + | latex::SUBPARAGRAPH => visit_section(context, node), + latex::ENUM_ITEM => visit_enum_item(context, node), + latex::EQUATION => visit_equation(context, node), + latex::ENVIRONMENT => latex::Environment::cast(node) + .and_then(|env| env.begin()) + .and_then(|begin| begin.name()) + .and_then(|name| name.word()) + .map(|name| name.text()) + .and_then(|name| { + if LANGUAGE_DATA + .math_environments + .iter() + .any(|env| env == name) + { + visit_equation_environment(context, node) + } else if LANGUAGE_DATA + .enum_environments + .iter() + .any(|env| env == name) + { + visit_enumeration(context, node, name) + } else if let Ok(float_kind) = LabelledFloatKind::from_str(name) { + visit_float(context, node, float_kind) + } else { + visit_theorem(context, node, name) + } + }), + _ => None, + }; + + match symbol { + Some(mut parent) => { + for child in node.children() { + if context.token.is_canceled() { + break; + } + + parent.children.append(&mut visit(context, child)); + } + vec![parent] + } + None => { + let mut symbols = Vec::new(); + for child in node.children() { + if context.token.is_canceled() { + break; + } + symbols.append(&mut visit(context, child)); + } + symbols + } + } +} + +fn visit_section(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> { + let section = latex::Section::cast(node)?; + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(section.small_range()); + + let group = section.name()?; + let group_text = group.content_text()?; + + let symbol = match find_label_by_parent(context, section.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("{} {}", number, group_text), + None => group_text.to_string(), + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Section, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name: group_text.to_string(), + label: None, + kind: InternalSymbolKind::Section, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_enum_item(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> { + let enum_item = latex::EnumItem::cast(node)?; + if !enum_item + .syntax() + .ancestors() + .filter_map(latex::Environment::cast) + .filter_map(|environment| environment.begin()) + .filter_map(|begin| begin.name()) + .filter_map(|name| name.word()) + .any(|name| { + LANGUAGE_DATA + .enum_environments + .iter() + .any(|e| e == name.text()) + }) + { + return None; + } + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(enum_item.small_range()); + + let name = enum_item + .label() + .and_then(|label| label.content_text()) + .unwrap_or_else(|| "Item".to_string()); + + let symbol = match find_label_by_parent(context, node) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => InternalSymbol { + name: number.map(Into::into).unwrap_or_else(|| name.to_string()), + label: Some(label), + kind: InternalSymbolKind::EnumerationItem, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + }, + None => InternalSymbol { + name: name.to_string(), + label: None, + kind: InternalSymbolKind::EnumerationItem, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_equation(context: &mut Context, node: &latex::SyntaxNode) -> Option<InternalSymbol> { + let equation = latex::Equation::cast(node)?; + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(equation.small_range()); + + make_equation_symbol(context, node, full_range) +} + +fn visit_equation_environment( + context: &mut Context, + node: &latex::SyntaxNode, +) -> Option<InternalSymbol> { + let environment = latex::Environment::cast(node)?; + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(environment.small_range()); + + make_equation_symbol(context, node, full_range) +} + +fn make_equation_symbol( + context: &mut Context, + node: &cstree::ResolvedNode<latex::Language>, + full_range: Range, +) -> Option<InternalSymbol> { + let symbol = match find_label_by_parent(context, node) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("Equation ({})", number), + None => "Equation".to_string(), + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Equation, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name: "Equation".to_string(), + label: None, + kind: InternalSymbolKind::Equation, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_enumeration( + context: &mut Context, + node: &latex::SyntaxNode, + env_name: &str, +) -> Option<InternalSymbol> { + let environment = latex::Environment::cast(node)?; + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(environment.small_range()); + + let name = titlecase(env_name); + let symbol = match find_label_by_parent(context, environment.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("{} {}", name, number), + None => name, + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Enumeration, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name, + label: None, + kind: InternalSymbolKind::Enumeration, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + Some(symbol) +} + +fn visit_float( + context: &mut Context, + node: &latex::SyntaxNode, + float_kind: LabelledFloatKind, +) -> Option<InternalSymbol> { + let environment = latex::Environment::cast(node)?; + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(environment.small_range()); + + let (float_kind, symbol_kind) = match float_kind { + LabelledFloatKind::Algorithm => ("Algorithm", InternalSymbolKind::Algorithm), + LabelledFloatKind::Figure => ("Figure", InternalSymbolKind::Figure), + LabelledFloatKind::Listing => ("Listing", InternalSymbolKind::Listing), + LabelledFloatKind::Table => ("Table", InternalSymbolKind::Table), + }; + + let caption = find_caption_by_parent(environment.syntax())?; + let symbol = match find_label_by_parent(context, environment.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match number { + Some(number) => format!("{} {}: {}", float_kind, number, caption), + None => format!("{}: {}", float_kind, caption), + }; + + InternalSymbol { + name, + label: Some(label), + kind: symbol_kind, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => InternalSymbol { + name: format!("{}: {}", float_kind, caption), + label: None, + kind: symbol_kind, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + }, + }; + + Some(symbol) +} + +fn visit_theorem( + context: &mut Context, + node: &latex::SyntaxNode, + environment_name: &str, +) -> Option<InternalSymbol> { + let definition = context + .subset + .documents + .iter() + .filter_map(|document| document.data.as_latex()) + .find_map(|data| { + data.extras + .theorem_environments + .iter() + .find(|environment| environment.name == environment_name) + .cloned() + })?; + + let node = latex::Environment::cast(node)?; + let theorem_description = node + .begin()? + .options() + .and_then(|option| option.content_text()); + + let full_range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(node.small_range()); + + let symbol = match find_label_by_parent(context, node.syntax()) { + Some(NumberedLabel { + name: label, + range: selection_range, + number, + }) => { + let name = match (number, theorem_description) { + (Some(number), Some(desc)) => { + format!("{} {} ({})", definition.description, number, desc) + } + (Some(number), None) => format!("{} {}", definition.description, number), + (None, Some(desc)) => format!("{} ({})", definition.description, desc), + (None, None) => definition.description.clone(), + }; + + InternalSymbol { + name, + label: Some(label), + kind: InternalSymbolKind::Theorem, + deprecated: false, + full_range, + selection_range, + children: Vec::new(), + } + } + None => { + let name = match theorem_description { + Some(desc) => format!("{} ({})", definition.description, desc), + None => definition.description.to_string(), + }; + InternalSymbol { + name, + label: None, + kind: InternalSymbolKind::Theorem, + deprecated: false, + full_range, + selection_range: full_range, + children: Vec::new(), + } + } + }; + Some(symbol) +} + +#[derive(Debug, PartialEq, Eq, Clone)] +struct NumberedLabel { + name: String, + range: Range, + number: Option<SmolStr>, +} + +fn find_label_by_parent( + context: &mut Context, + parent: &latex::SyntaxNode, +) -> Option<NumberedLabel> { + let node = parent.children().find_map(latex::LabelDefinition::cast)?; + + let name = node.name()?.word()?.text(); + let range = context + .subset + .documents + .first()? + .line_index + .line_col_lsp_range(node.small_range()); + + let number = find_label_number(&context.subset, name); + Some(NumberedLabel { + name: name.to_string(), + range, + number: number.map(Into::into), + }) +} |