use itertools::{EitherOrBoth, Itertools}; use rowan::{ast::AstNode, TextRange}; use super::{ LatexLanguage, SyntaxKind::{self, *}, SyntaxNode, SyntaxToken, }; pub fn small_range(node: &dyn AstNode) -> TextRange { let full_range = node.syntax().text_range(); let start = full_range.start(); let mut token = node.syntax().last_token(); while let Some(current) = token { if !matches!(current.kind(), LINE_BREAK | WHITESPACE | COMMENT) { return TextRange::new(start, current.text_range().end()); } token = current.prev_token(); } TextRange::new(start, start) } macro_rules! cst_node { ($name:ident, $($kind:pat),+) => { #[derive(Clone)] #[repr(transparent)] pub struct $name(SyntaxNode); impl AstNode for $name { type Language = LatexLanguage; fn can_cast(kind: SyntaxKind) -> bool { match kind { $($kind => true,)+ _ => false, } } fn cast(node: SyntaxNode) -> Option where Self: Sized, { match node.kind() { $($kind => Some(Self(node)),)+ _ => None, } } fn syntax(&self) -> &SyntaxNode { &self.0 } } }; } cst_node!(Text, TEXT); impl Text { pub fn words(&self) -> impl Iterator { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .filter(|node| node.kind() == WORD) } } pub trait HasCurly: AstNode { fn left_curly(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == L_CURLY) } fn right_curly(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == R_CURLY) } fn content_text(&self) -> Option { self.left_curly()?; self.right_curly()?; let mut text = String::new(); for child in self .syntax() .descendants_with_tokens() .filter_map(|child| child.into_token()) .filter(|token| !matches!(token.kind(), COMMENT)) { text.push_str(child.text()); } let text = text.trim_end(); let text = text[1..text.len() - 1].trim().to_string(); Some(text) } } cst_node!(CurlyGroup, CURLY_GROUP); impl HasCurly for CurlyGroup {} impl CurlyGroup {} pub trait HasBrack: AstNode { fn left_brack(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == L_BRACK) } fn right_brack(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == R_BRACK) } fn content_text(&self) -> Option { self.left_brack()?; self.right_brack()?; let mut text = String::new(); for child in self .syntax() .descendants_with_tokens() .filter_map(|child| child.into_token()) .filter(|token| !matches!(token.kind(), COMMENT)) { text.push_str(child.text()); } let text = text.trim_end(); let text = text[1..text.len() - 1].trim().to_string(); Some(text) } } cst_node!(BrackGroup, BRACK_GROUP); impl BrackGroup {} impl HasBrack for BrackGroup {} pub trait HasParen: AstNode { fn left_paren(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == L_PAREN) } fn right_paren(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == R_PAREN) } } cst_node!(ParenGroup, PAREN_GROUP); impl HasParen for ParenGroup {} cst_node!(MixedGroup, MIXED_GROUP); impl MixedGroup { pub fn left_delim(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| matches!(node.kind(), L_BRACK | L_PAREN)) } pub fn right_delim(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| matches!(node.kind(), R_BRACK | R_PAREN)) } } cst_node!(CurlyGroupWord, CURLY_GROUP_WORD); impl HasCurly for CurlyGroupWord {} impl CurlyGroupWord { pub fn key(&self) -> Option { self.syntax().children().find_map(Key::cast) } } cst_node!(BrackGroupWord, BRACK_GROUP_WORD); impl HasBrack for BrackGroupWord {} impl BrackGroupWord { pub fn key(&self) -> Option { self.syntax().children().find_map(Key::cast) } } cst_node!(CurlyGroupWordList, CURLY_GROUP_WORD_LIST); impl HasCurly for CurlyGroupWordList {} impl CurlyGroupWordList { pub fn keys(&self) -> impl Iterator { self.syntax().children().filter_map(Key::cast) } } cst_node!(CurlyGroupCommand, CURLY_GROUP_COMMAND); impl HasCurly for CurlyGroupCommand {} impl CurlyGroupCommand { pub fn command(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == COMMAND_NAME) } } cst_node!(Key, KEY); impl Key { pub fn words(&self) -> impl Iterator { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .filter(|node| !matches!(node.kind(), WHITESPACE | LINE_BREAK | COMMENT)) } } impl PartialEq for Key { fn eq(&self, other: &Self) -> bool { self.words() .zip_longest(other.words()) .all(|result| match result { EitherOrBoth::Both(left, right) => left.text() == right.text(), EitherOrBoth::Left(_) | EitherOrBoth::Right(_) => false, }) } } impl Eq for Key {} impl ToString for Key { fn to_string(&self) -> String { let mut buf = String::new(); for word in self.words() { buf.push_str(word.text()); buf.push(' '); } buf.pop().unwrap(); buf } } cst_node!(Value, VALUE); impl Value { pub fn text(&self) -> Option { match CurlyGroup::cast(self.syntax().clone()) { Some(group) => group.content_text(), None => Some(self.syntax().text().to_string()), } } } cst_node!(KeyValuePair, KEY_VALUE_PAIR); impl KeyValuePair { pub fn key(&self) -> Option { self.syntax().children().find_map(Key::cast) } pub fn value(&self) -> Option { self.syntax().children().find_map(Value::cast) } } cst_node!(KeyValueBody, KEY_VALUE_BODY); impl KeyValueBody { pub fn pairs(&self) -> impl Iterator { self.syntax().children().filter_map(KeyValuePair::cast) } } pub trait HasKeyValueBody: AstNode { fn body(&self) -> Option { self.syntax().children().find_map(KeyValueBody::cast) } } cst_node!(CurlyGroupKeyValue, CURLY_GROUP_KEY_VALUE); impl HasCurly for CurlyGroupKeyValue {} impl HasKeyValueBody for CurlyGroupKeyValue {} cst_node!(BrackGroupKeyValue, BRACK_GROUP_KEY_VALUE); impl HasBrack for BrackGroupKeyValue {} impl HasKeyValueBody for BrackGroupKeyValue {} cst_node!(Formula, FORMULA); cst_node!(GenericCommand, GENERIC_COMMAND); impl GenericCommand { pub fn name(&self) -> Option { self.syntax() .children_with_tokens() .filter_map(|node| node.into_token()) .find(|node| node.kind() == COMMAND_NAME) } } cst_node!(Equation, EQUATION); cst_node!(Begin, BEGIN); impl Begin { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } pub fn options(&self) -> Option { self.syntax().children().find_map(BrackGroup::cast) } } cst_node!(End, END); impl End { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } } cst_node!(Environment, ENVIRONMENT); impl Environment { pub fn begin(&self) -> Option { self.syntax().children().find_map(Begin::cast) } pub fn end(&self) -> Option { self.syntax().children().find_map(End::cast) } } cst_node!( Section, PART, CHAPTER, SECTION, SUBSECTION, SUBSUBSECTION, PARAGRAPH, SUBPARAGRAPH ); impl Section { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroup::cast) } } cst_node!(EnumItem, ENUM_ITEM); impl EnumItem { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn label(&self) -> Option { self.syntax().children().find_map(BrackGroup::cast) } } cst_node!(Caption, CAPTION); impl Caption { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn short(&self) -> Option { self.syntax().children().find_map(BrackGroup::cast) } pub fn long(&self) -> Option { self.syntax().children().find_map(CurlyGroup::cast) } } cst_node!(Citation, CITATION); impl Citation { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn prenote(&self) -> Option { self.syntax().children().find_map(BrackGroup::cast) } pub fn postnote(&self) -> Option { self.syntax().children().filter_map(BrackGroup::cast).nth(1) } pub fn key_list(&self) -> Option { self.syntax().children().find_map(CurlyGroupWordList::cast) } } cst_node!( Include, PACKAGE_INCLUDE, CLASS_INCLUDE, LATEX_INCLUDE, BIBLATEX_INCLUDE, BIBTEX_INCLUDE, GRAPHICS_INCLUDE, SVG_INCLUDE, INKSCAPE_INCLUDE, VERBATIM_INCLUDE ); impl Include { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn path_list(&self) -> Option { self.syntax().children().find_map(CurlyGroupWordList::cast) } } cst_node!(Import, IMPORT); impl Import { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn directory(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } pub fn file(&self) -> Option { self.syntax() .children() .filter_map(CurlyGroupWord::cast) .nth(1) } } cst_node!(LabelDefinition, LABEL_DEFINITION); impl LabelDefinition { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } } cst_node!(LabelReference, LABEL_REFERENCE); impl LabelReference { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name_list(&self) -> Option { self.syntax().children().find_map(CurlyGroupWordList::cast) } } cst_node!(LabelReferenceRange, LABEL_REFERENCE_RANGE); impl LabelReferenceRange { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn from(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } pub fn to(&self) -> Option { self.syntax() .children() .filter_map(CurlyGroupWord::cast) .nth(1) } } cst_node!(LabelNumber, LABEL_NUMBER); impl LabelNumber { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } pub fn text(&self) -> Option { self.syntax().children().find_map(CurlyGroup::cast) } } cst_node!( TheoremDefinition, THEOREM_DEFINITION_AMSTHM, THEOREM_DEFINITION_THMTOOLS ); impl TheoremDefinition { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } pub fn heading(&self) -> Option { if self.0.kind() == THEOREM_DEFINITION_THMTOOLS { let options = self .syntax() .children() .find_map(BrackGroupKeyValue::cast) .and_then(|group| group.body())?; options .pairs() .find(|pair| pair.key().map_or(false, |key| key.to_string() == "name")) .and_then(|pair| pair.value()) .and_then(|name| name.text()) } else { self.syntax() .children() .find_map(CurlyGroup::cast) .and_then(|group| group.content_text()) } } } cst_node!(CommandDefinition, COMMAND_DEFINITION, MATH_OPERATOR); impl CommandDefinition { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupCommand::cast) } pub fn implementation(&self) -> Option { self.syntax().children().find_map(CurlyGroup::cast) } } cst_node!(AcronymReference, ACRONYM_REFERENCE); impl AcronymReference { pub fn command(&self) -> Option { self.syntax().first_token() } } cst_node!(AcronymDefinition, ACRONYM_DEFINITION); impl AcronymDefinition { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } } cst_node!(AcronymDeclaration, ACRONYM_DECLARATION); impl AcronymDeclaration { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } } cst_node!(ColorDefinition, COLOR_DEFINITION); impl ColorDefinition { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } pub fn model(&self) -> Option { self.syntax() .children() .filter_map(CurlyGroupWord::cast) .nth(1) } pub fn spec(&self) -> Option { self.syntax().children().find_map(CurlyGroup::cast) } } cst_node!(ColorSetDefinition, COLOR_SET_DEFINITION); impl ColorSetDefinition { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn model_list(&self) -> Option { self.syntax().children().find_map(CurlyGroupWordList::cast) } } cst_node!(ColorReference, COLOR_REFERENCE); impl ColorReference { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } } cst_node!(GlossaryEntryReference, GLOSSARY_ENTRY_REFERENCE); impl GlossaryEntryReference { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } } cst_node!(GlossaryEntryDefinition, GLOSSARY_ENTRY_DEFINITION); impl GlossaryEntryDefinition { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name(&self) -> Option { self.syntax().children().find_map(CurlyGroupWord::cast) } } cst_node!(TikzLibraryImport, TIKZ_LIBRARY_IMPORT); impl TikzLibraryImport { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn name_list(&self) -> Option { self.syntax().children().find_map(CurlyGroupWordList::cast) } } cst_node!(GraphicsPath, GRAPHICS_PATH); impl GraphicsPath { pub fn command(&self) -> Option { self.syntax().first_token() } pub fn path_list(&self) -> impl Iterator { self.syntax().descendants().filter_map(CurlyGroupWord::cast) } }