diff options
Diffstat (limited to 'support/texlab/src/syntax/latex/analysis.rs')
-rw-r--r-- | support/texlab/src/syntax/latex/analysis.rs | 913 |
1 files changed, 913 insertions, 0 deletions
diff --git a/support/texlab/src/syntax/latex/analysis.rs b/support/texlab/src/syntax/latex/analysis.rs new file mode 100644 index 0000000000..af8b756f31 --- /dev/null +++ b/support/texlab/src/syntax/latex/analysis.rs @@ -0,0 +1,913 @@ +use crate::{ + protocol::{Options, Position, Range, RangeExt, Uri}, + syntax::{generic_ast::AstNodeIndex, lang_data::*, latex::ast::*, text::SyntaxNode}, + tex::Resolver, +}; +use itertools::{iproduct, Itertools}; +use serde::{Deserialize, Serialize}; +use std::{borrow::Cow, ops::Deref, path::Path}; + +#[derive(Debug, Clone)] +pub struct SymbolTableParams<'a> { + pub tree: Tree, + pub uri: &'a Uri, + pub resolver: &'a Resolver, + pub options: &'a Options, + pub current_dir: &'a Path, +} + +#[derive(Debug, Clone)] +pub struct SymbolTable { + pub(crate) tree: Tree, + pub commands: Vec<AstNodeIndex>, + pub environments: Vec<Environment>, + pub is_standalone: bool, + pub includes: Vec<Include>, + pub imports: Vec<Import>, + pub components: Vec<String>, + pub citations: Vec<Citation>, + pub command_definitions: Vec<CommandDefinition>, + pub glossary_entries: Vec<GlossaryEntry>, + pub equations: Vec<Equation>, + pub inlines: Vec<Inline>, + pub math_operators: Vec<MathOperator>, + pub theorem_definitions: Vec<TheoremDefinition>, + pub sections: Vec<Section>, + pub labels: Vec<Label>, + pub label_numberings: Vec<LabelNumbering>, + pub captions: Vec<Caption>, + pub items: Vec<Item>, +} + +impl SymbolTable { + pub fn analyze(params: SymbolTableParams) -> Self { + let SymbolTableParams { + tree, + uri, + resolver, + options, + current_dir, + } = params; + + let commands: Vec<_> = tree.commands().collect(); + let ctx = SymbolContext { + tree: &tree, + commands: &commands, + uri, + resolver, + options, + current_dir, + }; + + let mut environments = None; + let mut includes = None; + let mut imports = None; + let mut citations = None; + let mut command_definitions = None; + let mut glossary_entries = None; + let mut equations = None; + let mut inlines = None; + let mut math_operators = None; + let mut theorem_definitions = None; + let mut sections = None; + let mut labels = None; + let mut label_numberings = None; + let mut captions = None; + let mut items = None; + + rayon::scope(|s| { + s.spawn(|_| environments = Some(Environment::parse(ctx))); + s.spawn(|_| includes = Some(Include::parse(ctx))); + s.spawn(|_| imports = Some(Import::parse(ctx))); + s.spawn(|_| citations = Some(Citation::parse(ctx))); + s.spawn(|_| command_definitions = Some(CommandDefinition::parse(ctx))); + s.spawn(|_| glossary_entries = Some(GlossaryEntry::parse(ctx))); + s.spawn(|_| equations = Some(Equation::parse(ctx))); + s.spawn(|_| inlines = Some(Inline::parse(ctx))); + s.spawn(|_| math_operators = Some(MathOperator::parse(ctx))); + s.spawn(|_| theorem_definitions = Some(TheoremDefinition::parse(ctx))); + s.spawn(|_| sections = Some(Section::parse(ctx))); + s.spawn(|_| labels = Some(Label::parse(ctx))); + s.spawn(|_| label_numberings = Some(LabelNumbering::parse(ctx))); + s.spawn(|_| captions = Some(Caption::parse(ctx))); + s.spawn(|_| items = Some(Item::parse(ctx))); + }); + + let is_standalone = environments + .as_ref() + .unwrap() + .iter() + .any(|env| env.is_root(&tree)); + + let components = includes + .as_ref() + .unwrap() + .iter() + .flat_map(|include| include.components(&tree)) + .collect(); + + Self { + tree, + commands, + environments: environments.unwrap(), + is_standalone, + includes: includes.unwrap(), + imports: imports.unwrap(), + components, + citations: citations.unwrap(), + command_definitions: command_definitions.unwrap(), + glossary_entries: glossary_entries.unwrap(), + equations: equations.unwrap(), + inlines: inlines.unwrap(), + math_operators: math_operators.unwrap(), + theorem_definitions: theorem_definitions.unwrap(), + sections: sections.unwrap(), + labels: labels.unwrap(), + label_numberings: label_numberings.unwrap(), + captions: captions.unwrap(), + items: items.unwrap(), + } + } + + pub fn is_direct_child(&self, env: Environment, pos: Position) -> bool { + env.range(&self.tree).contains(pos) + && !self + .environments + .iter() + .filter(|e| e.left.parent != env.left.parent) + .filter(|e| env.range(&self.tree).contains(e.range(&self.tree).start)) + .any(|e| e.range(&self.tree).contains(pos)) + } + + pub fn is_enum_item(&self, enumeration: Environment, item: Item) -> bool { + let item_range = self.tree[item.parent].range(); + enumeration.range(&self.tree).contains(item_range.start) + && !self + .environments + .iter() + .filter(|env| env.left.parent != enumeration.left.parent) + .filter(|env| env.left.is_enum(&self.tree)) + .filter(|env| { + enumeration + .range(&self.tree) + .contains(env.range(&self.tree).start) + }) + .any(|env| env.range(&self.tree).contains(item_range.start)) + } + + pub fn find_label_by_range(&self, range: Range) -> Option<&Label> { + self.labels + .iter() + .filter(|label| label.kind == LatexLabelKind::Definition) + .filter(|label| label.names(&self).len() == 1) + .find(|label| range.contains(self[label.parent].range().start)) + } + + pub fn find_label_by_environment(&self, env: Environment) -> Option<&Label> { + self.labels + .iter() + .filter(|label| label.kind == LatexLabelKind::Definition) + .filter(|label| label.names(&self.tree).len() == 1) + .find(|label| self.is_direct_child(env, self.tree[label.parent].start())) + } +} + +impl Deref for SymbolTable { + type Target = Tree; + + fn deref(&self) -> &Self::Target { + &self.tree + } +} + +#[derive(Debug, Clone, Copy)] +pub struct SymbolContext<'a> { + tree: &'a Tree, + commands: &'a [AstNodeIndex], + uri: &'a Uri, + resolver: &'a Resolver, + options: &'a Options, + current_dir: &'a Path, +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct EnvironmentDelimiter { + pub parent: AstNodeIndex, +} + +impl EnvironmentDelimiter { + pub fn name(self, tree: &Tree) -> Option<&Token> { + tree.extract_word(self.parent, GroupKind::Group, 0) + } + + pub fn is_math(self, tree: &Tree) -> bool { + self.is_special(tree, LANGUAGE_DATA.math_environments.iter()) + } + + pub fn is_enum(self, tree: &Tree) -> bool { + self.is_special(tree, LANGUAGE_DATA.enum_environments.iter()) + } + + fn is_special<'a, I: Iterator<Item = &'a String>>(self, tree: &Tree, mut values: I) -> bool { + match self.name(tree) { + Some(name) => values.any(|env| env == name.text()), + None => false, + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Environment { + pub left: EnvironmentDelimiter, + pub right: EnvironmentDelimiter, +} + +impl Environment { + pub fn is_root(self, tree: &Tree) -> bool { + self.left + .name(tree) + .iter() + .chain(self.right.name(tree).iter()) + .any(|name| name.text() == "document") + } + + pub fn range(self, tree: &Tree) -> Range { + let start = tree[self.left.parent].start(); + let end = tree[self.right.parent].end(); + Range::new(start, end) + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + let mut stack = Vec::new(); + let mut envs = Vec::new(); + for parent in ctx.commands { + if let Some((delim, delim_cmd)) = Self::parse_delimiter(ctx.tree, *parent) { + if delim_cmd.name.text() == "\\begin" { + stack.push(delim); + } else if let Some(left) = stack.pop() { + envs.push(Self { left, right: delim }); + } + } + } + envs + } + + fn parse_delimiter( + tree: &Tree, + parent: AstNodeIndex, + ) -> Option<(EnvironmentDelimiter, &Command)> { + let cmd = tree.as_command(parent)?; + if cmd.name.text() != "\\begin" && cmd.name.text() != "\\end" { + return None; + } + + let group = tree.extract_group(parent, GroupKind::Group, 0)?; + if tree.extract_word(parent, GroupKind::Group, 0).is_some() + || tree.children(group).next().is_none() + || tree.as_group(group)?.right.is_none() + { + Some((EnvironmentDelimiter { parent }, cmd)) + } else { + None + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Include { + pub parent: AstNodeIndex, + pub arg_index: usize, + pub kind: LatexIncludeKind, + pub all_targets: Vec<Vec<Uri>>, + pub include_extension: bool, +} + +impl Include { + pub fn paths<'a>(&self, tree: &'a Tree) -> Vec<&'a Token> { + tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index) + .unwrap() + } + + pub fn components<'a>(&self, tree: &'a Tree) -> impl Iterator<Item = String> + 'a { + let kind = self.kind; + self.paths(tree) + .into_iter() + .filter_map(move |path| match kind { + LatexIncludeKind::Package => Some(format!("{}.sty", path.text())), + LatexIncludeKind::Class => Some(format!("{}.cls", path.text())), + LatexIncludeKind::Latex + | LatexIncludeKind::Bibliography + | LatexIncludeKind::Image + | LatexIncludeKind::Svg + | LatexIncludeKind::Pdf + | LatexIncludeKind::Everything => None, + }) + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + iproduct!(ctx.commands, LANGUAGE_DATA.include_commands.iter()) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &LatexIncludeCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + let mut all_targets = Vec::new(); + let paths = ctx + .tree + .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?; + for path in paths { + let mut targets = Vec::new(); + let base_url = base_url(ctx)?; + targets.push(base_url.join(path.text()).ok()?.into()); + + if let Some(extensions) = desc.kind.extensions() { + for extension in extensions { + let path = format!("{}.{}", path.text(), extension); + targets.push(base_url.join(&path).ok()?.into()); + } + } + + if let Some(target) = Self::resolve_distro_file(ctx, desc, path.text()) { + targets.push(target); + } + all_targets.push(targets); + } + + let include = Self { + parent, + arg_index: desc.index, + kind: desc.kind, + all_targets, + include_extension: desc.include_extension, + }; + Some(include) + } + + fn resolve_distro_file( + ctx: SymbolContext, + desc: &LatexIncludeCommand, + name: &str, + ) -> Option<Uri> { + let mut path = ctx.resolver.files_by_name.get(name); + if let Some(extensions) = desc.kind.extensions() { + for extension in extensions { + path = path.or_else(|| { + let full_name = format!("{}.{}", name, extension); + ctx.resolver.files_by_name.get(&full_name) + }); + } + } + path.and_then(|p| Uri::from_file_path(p).ok()) + } +} + +fn base_url(ctx: SymbolContext) -> Option<Uri> { + if let Some(root_directory) = ctx + .options + .latex + .as_ref() + .and_then(|opts| opts.root_directory.as_ref()) + { + let file_name = ctx.uri.path_segments()?.last()?; + let path = ctx.current_dir.join(root_directory).join(file_name); + Uri::from_file_path(path).ok() + } else { + Some(ctx.uri.clone()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Import { + pub parent: AstNodeIndex, + pub targets: Vec<Uri>, +} + +impl Import { + pub fn dir<'a>(&self, tree: &'a Tree) -> &'a Token { + tree.extract_word(self.parent, GroupKind::Group, 0).unwrap() + } + + pub fn file<'a>(&self, tree: &'a Tree) -> &'a Token { + tree.extract_word(self.parent, GroupKind::Group, 1).unwrap() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + ctx.commands + .iter() + .filter_map(|parent| Self::parse_single(ctx, *parent)) + .collect() + } + + fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != "\\import" && cmd.name.text() != "\\subimport" { + return None; + } + + let dir = ctx.tree.extract_word(parent, GroupKind::Group, 0)?; + let file = ctx.tree.extract_word(parent, GroupKind::Group, 1)?; + + let mut targets = Vec::new(); + let base_url = base_url(ctx)?.join(dir.text()).ok()?; + targets.push(base_url.join(file.text()).ok()?.into()); + targets.push(base_url.join(&format!("{}.tex", file.text())).ok()?.into()); + Some(Self { parent, targets }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Citation { + parent: AstNodeIndex, + arg_index: usize, +} + +impl Citation { + pub fn keys(self, tree: &Tree) -> Vec<&Token> { + tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index) + .unwrap() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + iproduct!(ctx.commands, LANGUAGE_DATA.citation_commands.iter()) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &LatexCitationCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + ctx.tree + .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?; + + Some(Self { + parent, + arg_index: desc.index, + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct CommandDefinition { + pub parent: AstNodeIndex, + pub definition: AstNodeIndex, + pub definition_index: usize, + pub implementation: AstNodeIndex, + pub implementation_index: usize, + pub arg_count_index: usize, +} + +impl CommandDefinition { + pub fn definition_name(self, tree: &Tree) -> &str { + tree.as_command(self.definition).unwrap().name.text() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + let def = LANGUAGE_DATA.command_definition_commands.iter(); + iproduct!(ctx.commands, def) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &LatexCommandDefinitionCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + let group_kind = GroupKind::Group; + let implementation = + ctx.tree + .extract_group(parent, group_kind, desc.implementation_index)?; + + let def_group = ctx + .tree + .extract_group(parent, group_kind, desc.definition_index)?; + + let mut def_children = ctx.tree.children(def_group); + let definition = def_children.next()?; + ctx.tree.as_command(definition)?; + Some(Self { + parent, + definition, + definition_index: desc.definition_index, + implementation, + implementation_index: desc.implementation_index, + arg_count_index: desc.arg_count_index, + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct GlossaryEntry { + pub parent: AstNodeIndex, + pub label_index: usize, + pub kind: LatexGlossaryEntryKind, +} + +impl GlossaryEntry { + pub fn label(self, tree: &Tree) -> &Token { + tree.extract_word(self.parent, GroupKind::Group, self.label_index) + .unwrap() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + let entry = LANGUAGE_DATA.glossary_entry_definition_commands.iter(); + iproduct!(ctx.commands, entry) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &LatexGlossaryEntryDefinitionCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + ctx.tree + .extract_word(parent, GroupKind::Group, desc.label_index)?; + + Some(Self { + parent, + label_index: desc.label_index, + kind: desc.kind, + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Equation { + pub left: AstNodeIndex, + pub right: AstNodeIndex, +} + +impl Equation { + pub fn range(self, tree: &Tree) -> Range { + let start = tree[self.left].start(); + let end = tree[self.right].end(); + Range::new(start, end) + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + let mut equations = Vec::new(); + let mut left = None; + for node in ctx.commands { + let cmd = ctx.tree.as_command(*node).unwrap(); + let name = cmd.name.text(); + if name == "\\[" || name == "\\(" { + left = Some(node); + } else if name == "\\]" || name == "\\)" { + if let Some(begin) = left { + equations.push(Self { + left: *begin, + right: *node, + }); + left = None; + } + } + } + equations + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Inline { + pub left: AstNodeIndex, + pub right: AstNodeIndex, +} + +impl Inline { + pub fn range(self, tree: &Tree) -> Range { + let start = tree[self.left].start(); + let end = tree[self.right].end(); + Range::new(start, end) + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + let mut inlines = Vec::new(); + let mut left = None; + for node in ctx + .tree + .nodes() + .into_iter() + .filter(|node| ctx.tree.as_math(*node).is_some()) + .sorted_by_key(|node| ctx.tree[*node].start()) + { + if let Some(l) = left { + inlines.push(Inline { + left: l, + right: node, + }); + left = None; + } else { + left = Some(node); + } + } + inlines + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct MathOperator { + pub parent: AstNodeIndex, + pub definition: AstNodeIndex, + pub definition_index: usize, + pub implementation: AstNodeIndex, + pub implementation_index: usize, +} + +impl MathOperator { + pub fn definition_name(self, tree: &Tree) -> &str { + tree.as_command(self.definition).unwrap().name.text() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + iproduct!(ctx.commands, LANGUAGE_DATA.math_operator_commands.iter()) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &LatexMathOperatorCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + let group_kind = GroupKind::Group; + let def_group = ctx + .tree + .extract_group(parent, group_kind, desc.definition_index)?; + let implementation = + ctx.tree + .extract_group(parent, group_kind, desc.implementation_index)?; + + let mut def_children = ctx.tree.children(def_group); + let definition = def_children.next()?; + Some(Self { + parent, + definition, + definition_index: desc.definition_index, + implementation, + implementation_index: desc.implementation_index, + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct TheoremDefinition { + pub parent: AstNodeIndex, + pub arg_index: usize, +} + +impl TheoremDefinition { + pub fn name(self, tree: &Tree) -> &Token { + tree.extract_word(self.parent, GroupKind::Group, self.arg_index) + .unwrap() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + let thm = LANGUAGE_DATA.theorem_definition_commands.iter(); + iproduct!(ctx.commands, thm) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &LatexTheoremDefinitionCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + let group_kind = GroupKind::Group; + ctx.tree.extract_word(parent, group_kind, desc.index)?; + + Some(Self { + parent, + arg_index: desc.index, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Section { + pub parent: AstNodeIndex, + pub arg_index: usize, + pub level: i32, + pub prefix: Cow<'static, str>, +} + +impl Section { + pub fn print(&self, tree: &Tree) -> Option<String> { + tree.print_group_content(self.parent, GroupKind::Group, self.arg_index) + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + iproduct!(ctx.commands, LANGUAGE_DATA.section_commands.iter()) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &'static LatexSectionCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + let group_kind = GroupKind::Group; + ctx.tree.extract_group(parent, group_kind, desc.index)?; + + Some(Self { + parent, + arg_index: desc.index, + level: desc.level, + prefix: Cow::from(&desc.prefix), + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Label { + pub parent: AstNodeIndex, + pub arg_index: usize, + pub kind: LatexLabelKind, +} + +impl Label { + pub fn names(self, tree: &Tree) -> Vec<&Token> { + tree.extract_comma_separated_words(self.parent, GroupKind::Group, self.arg_index) + .unwrap() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + iproduct!(ctx.commands, LANGUAGE_DATA.label_commands.iter()) + .filter_map(|(parent, desc)| Self::parse_single(ctx, *parent, desc)) + .collect() + } + + fn parse_single( + ctx: SymbolContext, + parent: AstNodeIndex, + desc: &LatexLabelCommand, + ) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != desc.name { + return None; + } + + ctx.tree + .extract_comma_separated_words(parent, GroupKind::Group, desc.index)?; + + Some(Self { + parent, + arg_index: desc.index, + kind: desc.kind, + }) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LabelNumbering { + pub parent: AstNodeIndex, + pub number: String, +} + +impl LabelNumbering { + pub fn name<'a>(&self, tree: &'a Tree) -> &'a Token { + tree.extract_word(self.parent, GroupKind::Group, 0).unwrap() + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + ctx.commands + .iter() + .filter_map(|parent| Self::parse_single(ctx, *parent)) + .collect() + } + + fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != "\\newlabel" { + return None; + } + + ctx.tree.extract_word(parent, GroupKind::Group, 0)?; + + let arg = ctx.tree.extract_group(parent, GroupKind::Group, 1)?; + let mut analyzer = FirstText::default(); + analyzer.visit(ctx.tree, arg); + Some(Self { + parent, + number: analyzer.text?, + }) + } +} + +#[derive(Debug, Default)] +struct FirstText { + text: Option<String>, +} + +impl Visitor for FirstText { + fn visit(&mut self, tree: &Tree, node: AstNodeIndex) { + if let Some(text) = tree.as_text(node) { + self.text = Some(text.words.iter().map(Token::text).join(" ")); + } + + if self.text.is_none() { + tree.walk(self, node); + } + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Caption { + pub parent: AstNodeIndex, + pub arg_index: usize, +} + +impl Caption { + pub fn print(self, tree: &Tree) -> Option<String> { + tree.print_group_content(self.parent, GroupKind::Group, self.arg_index) + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + ctx.commands + .iter() + .flat_map(|parent| Self::parse_single(ctx, *parent)) + .collect() + } + + fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != "\\caption" { + return None; + } + + ctx.tree.extract_group(parent, GroupKind::Group, 0)?; + Some(Self { + parent, + arg_index: 0, + }) + } +} + +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct Item { + pub parent: AstNodeIndex, +} + +impl Item { + pub fn name(self, tree: &Tree) -> Option<String> { + tree.print_group_content(self.parent, GroupKind::Options, 0) + } + + fn parse(ctx: SymbolContext) -> Vec<Self> { + ctx.commands + .iter() + .filter_map(|parent| Self::parse_single(ctx, *parent)) + .collect() + } + + fn parse_single(ctx: SymbolContext, parent: AstNodeIndex) -> Option<Self> { + let cmd = ctx.tree.as_command(parent)?; + if cmd.name.text() != "\\item" { + return None; + } + + Some(Self { parent }) + } +} |