use crate::{ feature::DocumentView, protocol::{MarkupContent, MarkupKind, Options, Position, Range, RangeExt, Uri}, syntax::{latex, SyntaxNode}, workspace::{Document, DocumentContent}, }; use std::{borrow::Cow, collections::HashSet, path::Path}; use titlecase::titlecase; use OutlineContextItem::*; #[derive(Debug, Clone, Copy)] pub struct OutlineSection<'a> { pub document: &'a Document, pub item: &'a latex::Section, } pub struct Outline<'a> { sections: Vec>, } impl<'a> Outline<'a> { pub fn find(&'a self, uri: &Uri, pos: Position) -> Option<&'a latex::Section> { self.sections .iter() .filter(|section| section.document.uri == *uri) .rev() .find(|section| { let table = section.document.content.as_latex().unwrap(); table[section.item.parent].end() <= pos }) .map(|sec| sec.item) } pub fn analyze(view: &'a DocumentView, options: &Options, current_dir: &Path) -> Self { let mut finder = OutlineSectionFinder::default(); let doc = view .snapshot .parent(&view.current.uri, options, current_dir) .map(|parent| { view.related .iter() .find(|doc| doc.uri == parent.uri) .unwrap() }) .unwrap_or(&view.current); finder.analyze(view, doc); Self { sections: finder.sections, } } } #[derive(Debug, Clone, Copy)] enum OutlineItem<'a> { Section(&'a latex::Section), Include(&'a latex::Include), } #[derive(Debug, Default)] struct OutlineSectionFinder<'a> { visited: HashSet<&'a Uri>, sections: Vec>, } impl<'a> OutlineSectionFinder<'a> { fn analyze(&mut self, view: &'a DocumentView, doc: &'a Document) { if !self.visited.insert(&doc.uri) { return; } if let DocumentContent::Latex(table) = &doc.content { let mut items = Vec::new(); for section in &table.sections { items.push(OutlineItem::Section(section)); } for include in &table.includes { items.push(OutlineItem::Include(include)); } items.sort_by_key(|item| match item { OutlineItem::Include(include) => table[include.parent].start(), OutlineItem::Section(section) => table[section.parent].start(), }); for item in items { match item { OutlineItem::Section(item) => { let section = OutlineSection { document: doc, item, }; self.sections.push(section); } OutlineItem::Include(item) => { for doc in &view.related { for targets in &item.all_targets { if targets.contains(&doc.uri) { self.analyze(view, doc); break; } } } } } } } } } #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum OutlineCaptionKind { Figure, Table, Listing, Algorithm, } impl OutlineCaptionKind { pub fn as_str(self) -> &'static str { match self { Self::Figure => "Figure", Self::Table => "Table", Self::Listing => "Listing", Self::Algorithm => "Algorithm", } } pub fn parse(env_name: &str) -> Option { match env_name { "figure" | "subfigure" => Some(Self::Figure), "table" | "subtable" => Some(Self::Table), "listing" | "lstlisting" => Some(Self::Listing), "algorithm" => Some(Self::Algorithm), _ => None, } } } #[derive(Debug, PartialEq, Eq, Clone)] pub enum OutlineContextItem { Section { prefix: Cow<'static, str>, text: String, }, Caption { kind: Option, text: String, }, Theorem { kind: String, description: Option, }, Equation, Item, } #[derive(Debug, PartialEq, Eq, Clone)] pub struct OutlineContext { pub range: Range, pub number: Option, pub item: OutlineContextItem, } impl OutlineContext { pub fn reference(&self) -> String { match &self.number { Some(number) => match &self.item { Section { prefix, text } => format!("{} {} ({})", prefix, number, text), Caption { kind: None, text } => format!("{} {}", number, text), Caption { kind: Some(kind), text, } => format!("{} {}: {}", kind.as_str(), number, text), Theorem { kind, description: None, } => format!("{} {}", kind, number), Theorem { kind, description: Some(description), } => format!("{} {} ({})", kind, number, description), Equation => format!("Equation ({})", number), Item => format!("Item {}", number), }, None => match &self.item { Section { prefix, text } => format!("{} ({})", prefix, text), Caption { kind: None, text } => text.clone(), Caption { kind: Some(kind), text, } => format!("{}: {}", kind.as_str(), text), Theorem { kind, description: None, } => kind.into(), Theorem { kind, description: Some(description), } => format!("{} ({})", kind, description), Equation => "Equation".into(), Item => "Item".into(), }, } } pub fn detail(&self) -> Option { match &self.item { Section { .. } | Theorem { .. } | Equation | Item => Some(self.reference()), Caption { kind: Some(kind), .. } => { let result = match &self.number { Some(number) => format!("{} {}", kind.as_str(), number), None => kind.as_str().to_owned(), }; Some(result) } Caption { .. } => None, } } pub fn documentation(&self) -> MarkupContent { MarkupContent { kind: MarkupKind::PlainText, value: self.reference(), } } pub fn parse(view: &DocumentView, outline: &Outline, label: latex::Label) -> Option { if let DocumentContent::Latex(table) = &view.current.content { Self::find_caption(view, table, label) .or_else(|| Self::find_theorem(view, table, label)) .or_else(|| Self::find_equation(view, table, label)) .or_else(|| Self::find_item(view, table, label)) .or_else(|| Self::find_section(view, outline, table, label)) } else { None } } fn find_caption( view: &DocumentView, table: &latex::SymbolTable, label: latex::Label, ) -> Option { let label_range = table[label.parent].range(); let caption_env = table .environments .iter() .filter(|env| !env.is_root(&table)) .find(|env| env.range(&table).contains(label_range.start))?; let caption = table .captions .iter() .find(|cap| table.is_direct_child(*caption_env, table[cap.parent].start()))?; let caption_text = table.print_group_content( caption.parent, latex::GroupKind::Group, caption.arg_index, )?; let caption_kind = caption_env .left .name(&table) .map(latex::Token::text) .and_then(OutlineCaptionKind::parse); Some(Self { range: caption_env.range(&table), number: Self::find_number(view, table, label), item: Caption { kind: caption_kind, text: caption_text, }, }) } fn find_theorem( view: &DocumentView, main_table: &latex::SymbolTable, label: latex::Label, ) -> Option { let label_range = main_table[label.parent].range(); let env = main_table .environments .iter() .find(|env| env.range(&main_table).contains(label_range.start))?; let env_name = env.left.name(&main_table).map(latex::Token::text)?; for doc in &view.related { if let DocumentContent::Latex(table) = &doc.content { for def in &table.theorem_definitions { if env_name == def.name(&table).text() { let kind = table .print_group_content( def.parent, latex::GroupKind::Group, def.arg_index + 1, ) .unwrap_or_else(|| titlecase(&env_name)); let description = main_table.print_group_content( env.left.parent, latex::GroupKind::Options, 0, ); return Some(Self { range: env.range(&main_table), number: Self::find_number(view, main_table, label), item: Theorem { kind, description }, }); } } } } None } fn find_equation( view: &DocumentView, table: &latex::SymbolTable, label: latex::Label, ) -> Option { let label_range = table[label.parent].range(); table .environments .iter() .filter(|env| env.left.is_math(&table)) .map(|env| env.range(&table)) .find(|range| range.contains(label_range.start)) .map(|range| Self { range, number: Self::find_number(view, table, label), item: Equation, }) } fn find_item( view: &DocumentView, table: &latex::SymbolTable, label: latex::Label, ) -> Option { let label_range = table[label.parent].range(); struct LatexItemNode { item: latex::Item, range: Range, } let enumeration = table .environments .iter() .filter(|env| env.left.is_enum(&table)) .find(|env| env.range(&table).contains(label_range.start))?; let mut item_nodes: Vec<_> = table .items .iter() .filter(|item| table.is_enum_item(*enumeration, **item)) .map(|item| LatexItemNode { item: *item, range: Range::default(), }) .collect(); for i in 0..item_nodes.len() { let start = table[item_nodes[i].item.parent].start(); let end = item_nodes .get(i + 1) .map(|node| table[node.item.parent].start()) .unwrap_or_else(|| table[enumeration.right.parent].start()); item_nodes[i].range = Range::new(start, end); } let node = item_nodes .iter() .find(|node| node.range.contains(label_range.start))?; let number = node .item .name(&table) .or_else(|| Self::find_number(view, table, label)); Some(Self { range: enumeration.range(&table), number, item: Item, }) } fn find_section( view: &DocumentView, outline: &Outline, table: &latex::SymbolTable, label: latex::Label, ) -> Option { let label_range = table[label.parent].range(); let section = outline.find(&view.current.uri, label_range.start)?; Some(Self { range: table[section.parent].range(), number: Self::find_number(view, table, label), item: Section { prefix: section.prefix.clone(), text: table.print_group_content( section.parent, latex::GroupKind::Group, section.arg_index, )?, }, }) } pub fn find_number( view: &DocumentView, table: &latex::SymbolTable, label: latex::Label, ) -> Option { let label_names = label.names(&table); if label_names.len() != 1 { return None; } for doc in &view.related { if let DocumentContent::Latex(table) = &doc.content { for numbering in &table.label_numberings { if numbering.name(&table).text() == label_names[0].text() { return Some(numbering.number.clone()); } } } } None } }