use std::str::FromStr; use lsp_types::{MarkupContent, MarkupKind}; use rowan::{ast::AstNode, TextRange}; use crate::{ syntax::latex::{self, HasBrack, HasCurly}, Workspace, LANGUAGE_DATA, }; use self::LabelledObject::*; #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum LabelledFloatKind { Figure, Table, Listing, Algorithm, } impl LabelledFloatKind { pub fn as_str(self) -> &'static str { match self { Self::Figure => "Figure", Self::Table => "Table", Self::Listing => "Listing", Self::Algorithm => "Algorithm", } } } impl FromStr for LabelledFloatKind { type Err = (); fn from_str(s: &str) -> Result { match s { "figure" | "subfigure" => Ok(Self::Figure), "table" | "subtable" => Ok(Self::Table), "listing" | "lstlisting" => Ok(Self::Listing), "algorithm" => Ok(Self::Algorithm), _ => Err(()), } } } #[derive(Debug, PartialEq, Eq, Clone)] pub enum LabelledObject { Section { prefix: &'static str, text: String, }, Float { kind: LabelledFloatKind, caption: String, }, Theorem { kind: String, description: Option, }, Equation, EnumItem, } #[derive(Debug, PartialEq, Eq, Clone)] pub struct RenderedLabel { pub range: TextRange, pub number: Option, pub object: LabelledObject, } impl RenderedLabel { #[must_use] pub fn reference(&self) -> String { match &self.number { Some(number) => match &self.object { Section { prefix, text } => format!("{} {} ({})", prefix, number, text), Float { kind, caption } => format!("{} {}: {}", kind.as_str(), number, caption), Theorem { kind, description: None, } => format!("{} {}", kind, number), Theorem { kind, description: Some(description), } => format!("{} {} ({})", kind, number, description), Equation => format!("Equation ({})", number), EnumItem => format!("Item {}", number), }, None => match &self.object { Section { prefix, text } => format!("{} ({})", prefix, text), Float { kind, caption } => format!("{}: {}", kind.as_str(), caption), Theorem { kind, description: None, } => kind.into(), Theorem { kind, description: Some(description), } => format!("{} ({})", kind, description), Equation => "Equation".into(), EnumItem => "Item".into(), }, } } #[must_use] pub fn detail(&self) -> Option { match &self.object { Section { .. } | Theorem { .. } | Equation | EnumItem => Some(self.reference()), Float { kind, .. } => { let result = match &self.number { Some(number) => format!("{} {}", kind.as_str(), number), None => kind.as_str().to_owned(), }; Some(result) } } } #[must_use] pub fn documentation(&self) -> MarkupContent { MarkupContent { kind: MarkupKind::PlainText, value: self.reference(), } } } pub fn render_label( workspace: &Workspace, label_name: &str, mut label: Option, ) -> Option { let mut number = find_label_number(workspace, label_name).map(ToString::to_string); for document in workspace.documents_by_uri.values() { if let Some(data) = document.data.as_latex() { label = label.or_else(|| { find_label_definition(&latex::SyntaxNode::new_root(data.green.clone()), label_name) }); } } label?.syntax().ancestors().find_map(|parent| { render_label_float(parent.clone(), &mut number) .or_else(|| render_label_section(parent.clone(), &mut number)) .or_else(|| render_label_enum_item(parent.clone(), &mut number)) .or_else(|| render_label_equation(parent.clone(), &mut number)) .or_else(|| render_label_theorem(workspace, parent, &mut number)) }) } pub fn find_label_definition( root: &latex::SyntaxNode, label_name: &str, ) -> Option { root.descendants() .filter_map(latex::LabelDefinition::cast) .find(|label| { label .name() .and_then(|name| name.key()) .map(|name| name.to_string()) .as_deref() == Some(label_name) }) } pub fn find_label_number<'a>(workspace: &'a Workspace, label_name: &str) -> Option<&'a str> { workspace.documents_by_uri.values().find_map(|document| { document .data .as_latex() .and_then(|data| data.extras.label_numbers_by_name.get(label_name)) .map(|number| number.as_str()) }) } fn render_label_float( parent: latex::SyntaxNode, number: &mut Option, ) -> Option { let environment = latex::Environment::cast(parent.clone())?; let environment_name = environment.begin()?.name()?.key()?.to_string(); let kind = LabelledFloatKind::from_str(&environment_name).ok()?; let caption = find_caption_by_parent(&parent)?; Some(RenderedLabel { range: latex::small_range(&environment), number: number.take(), object: LabelledObject::Float { caption, kind }, }) } fn render_label_section( parent: latex::SyntaxNode, number: &mut Option, ) -> Option { let section = latex::Section::cast(parent)?; let text_group = section.name()?; let text = text_group.content_text()?; Some(RenderedLabel { range: latex::small_range(§ion), number: number.take(), object: LabelledObject::Section { prefix: match section.syntax().kind() { latex::PART => "Part", latex::CHAPTER => "Chapter", latex::SECTION => "Section", latex::SUBSECTION => "Subsection", latex::SUBSUBSECTION => "Subsubsection", latex::PARAGRAPH => "Paragraph", latex::SUBPARAGRAPH => "Subparagraph", _ => unreachable!(), }, text, }, }) } fn render_label_enum_item( parent: latex::SyntaxNode, number: &mut Option, ) -> Option { let enum_item = latex::EnumItem::cast(parent)?; Some(RenderedLabel { range: latex::small_range(&enum_item), number: enum_item .label() .and_then(|number| number.content_text()) .or_else(|| number.take()), object: LabelledObject::EnumItem, }) } fn render_label_equation( parent: latex::SyntaxNode, number: &mut Option, ) -> Option { let environment = latex::Environment::cast(parent)?; let environment_name = environment.begin()?.name()?.key()?.to_string(); if !LANGUAGE_DATA .math_environments .iter() .any(|name| name == &environment_name) { return None; } Some(RenderedLabel { range: latex::small_range(&environment), number: number.take(), object: LabelledObject::Equation, }) } fn render_label_theorem( workspace: &Workspace, parent: latex::SyntaxNode, number: &mut Option, ) -> Option { let environment = latex::Environment::cast(parent)?; let begin = environment.begin()?; let description = begin.options().and_then(|options| options.content_text()); let environment_name = begin.name()?.key()?.to_string(); let theorem = workspace.documents_by_uri.values().find_map(|document| { document.data.as_latex().and_then(|data| { data.extras .theorem_environments .iter() .find(|theorem| theorem.name.as_str() == environment_name) }) })?; Some(RenderedLabel { range: latex::small_range(&environment), number: number.take(), object: LabelledObject::Theorem { kind: theorem.description.clone(), description, }, }) } pub fn find_caption_by_parent(parent: &latex::SyntaxNode) -> Option { parent .children() .filter_map(latex::Caption::cast) .find_map(|node| node.long()) .and_then(|node| node.content_text()) }